Merge remote-tracking branch 'origin/opengl-capture'

This commit is contained in:
David Vogel 2024-01-15 21:29:23 +01:00
commit d82fda528a
7 changed files with 94 additions and 82 deletions

View File

@ -13,6 +13,8 @@
"downscaling", "downscaling",
"DPMM", "DPMM",
"executables", "executables",
"framebuffer",
"framebuffers",
"Fullscreen", "Fullscreen",
"goarch", "goarch",
"gridify", "gridify",

View File

@ -1,4 +1,4 @@
; Copyright (c) 2019-2022 David Vogel ; Copyright (c) 2019-2023 David Vogel
; ;
; This software is released under the MIT License. ; This software is released under the MIT License.
; https://opensource.org/licenses/MIT ; https://opensource.org/licenses/MIT
@ -15,44 +15,37 @@ Structure QueueElement
sy.i sy.i
EndStructure EndStructure
; Source: https://www.purebasic.fr/english/viewtopic.php?f=13&t=29981&start=15 Structure GLViewportDims
Procedure EnumWindowsProc(hWnd.l, *lParam.Long) x.i
Protected lpProc.l y.i
GetWindowThreadProcessId_(hWnd, @lpProc) width.i
If *lParam\l = lpProc ; Check if current window's processID matches height.i
*lParam\l = hWnd ; Replace processID in the param With the hwnd As result EndStructure
ProcedureReturn #False ; Return false to stop iterating
; Returns the size of the main OpenGL rendering output.
ProcedureDLL GetGLViewportSize(*dims.GLViewportDims)
If Not *dims
ProcedureReturn #False
EndIf EndIf
glGetIntegerv_(#GL_VIEWPORT, *dims)
ProcedureReturn #True ProcedureReturn #True
EndProcedure EndProcedure
; Source: https://www.purebasic.fr/english/viewtopic.php?f=13&t=29981&start=15 ; Returns the size of the main OpenGL rendering output as a windows RECT.
; Returns the first window associated with the given process handle
Procedure GetProcHwnd()
Protected pID.l = GetCurrentProcessId_()
Protected tempParam.l = pID
EnumWindows_(@EnumWindowsProc(), @tempParam)
If tempParam = pID ; Check if anything was found
ProcedureReturn #Null
EndIf
ProcedureReturn tempParam ; This is a valid hWnd at this point
EndProcedure
; Get the client rectangle of the "Main" window of this process in screen coordinates
ProcedureDLL GetRect(*rect.RECT) ProcedureDLL GetRect(*rect.RECT)
Protected hWnd.l = GetProcHwnd()
If Not hWnd
ProcedureReturn #False
EndIf
If Not *rect If Not *rect
ProcedureReturn #False ProcedureReturn #False
EndIf EndIf
GetClientRect_(hWnd, *rect) Protected dims.GLViewportDims
glGetIntegerv_(#GL_VIEWPORT, dims)
; A RECT consists basically of two POINT structures *rect\left = dims\x
ClientToScreen_(hWnd, @*rect\left) *rect\top = dims\y
ClientToScreen_(hWnd, @*rect\Right) *rect\right = dims\x + dims\width
*rect\bottom = dims\y + dims\height
ProcedureReturn #True ProcedureReturn #True
EndProcedure EndProcedure
@ -97,53 +90,56 @@ Procedure Worker(*Dummy)
EndProcedure EndProcedure
; Takes a screenshot of the client area of this process' active window. ; Takes a screenshot of the client area of this process' active window.
; The portion of the client area that is captured is described by capRect, which is in window coordinates and relative to the client area. ; The portion of the client area that is captured is described by capRect, which is in viewport coordinates.
; x and y defines the top left position of the captured rectangle in scaled world coordinates. The scale depends on the window to world pixel ratio. ; x and y defines the top left position of the captured rectangle in scaled world coordinates. The scale depends on the window to world pixel ratio.
; sx and sy defines the final dimensions that the screenshot will be resized to. No resize will happen if set to 0. ; sx and sy defines the final dimensions that the screenshot will be resized to. No resize will happen if set to 0.
ProcedureDLL Capture(*capRect.RECT, x.l, y.l, sx.l, sy.l) ProcedureDLL Capture(*capRect.RECT, x.l, y.l, sx.l, sy.l)
Protected hWnd.l = GetProcHwnd() Protected viewportRect.RECT
If Not hWnd If Not GetRect(@viewportRect)
ProcedureReturn #False ProcedureReturn #False
EndIf EndIf
Protected rect.RECT ; Limit the desired capture area to the actual client area of the viewport.
If Not GetRect(@rect)
ProcedureReturn #False
EndIf
; Limit the desired capture area to the actual client area of the window.
If *capRect\left < 0 : *capRect\left = 0 : EndIf If *capRect\left < 0 : *capRect\left = 0 : EndIf
If *capRect\right > rect\right-rect\left : *capRect\right = rect\right-rect\left : EndIf
If *capRect\top < 0 : *capRect\top = 0 : EndIf If *capRect\top < 0 : *capRect\top = 0 : EndIf
If *capRect\bottom > rect\bottom-rect\top : *capRect\bottom = rect\bottom-rect\top : EndIf If *capRect\right < *capRect\left : *capRect\right = *capRect\left : EndIf
If *capRect\bottom < *capRect\top : *capRect\bottom = *capRect\top : EndIf
If *capRect\right > viewportRect\right : *capRect\right = viewportRect\right : EndIf
If *capRect\bottom > viewportRect\bottom : *capRect\bottom = viewportRect\bottom : EndIf
imageID = CreateImage(#PB_Any, *capRect\right-*capRect\left, *capRect\bottom-*capRect\top) Protected capWidth = *capRect\right - *capRect\left
Protected capHeight = *capRect\bottom - *capRect\top
imageID = CreateImage(#PB_Any, capWidth, capHeight)
If Not imageID If Not imageID
ProcedureReturn #False ProcedureReturn #False
EndIf EndIf
; Get DC of window. ;Protected *pixelBuf = AllocateMemory(3 * width * height)
windowDC = GetDC_(hWnd)
If Not windowDC
FreeImage(imageID)
ProcedureReturn #False
EndIf
hDC = StartDrawing(ImageOutput(imageID)) hDC = StartDrawing(ImageOutput(imageID))
If Not hDC If Not hDC
ReleaseDC_(hWnd, windowDC)
FreeImage(imageID) FreeImage(imageID)
ProcedureReturn #False ProcedureReturn #False
EndIf EndIf
If Not BitBlt_(hDC, 0, 0, *capRect\right-*capRect\left, *capRect\bottom-*capRect\top, windowDC, *capRect\left, *capRect\top, #SRCCOPY) ; After some time BitBlt will fail, no idea why. Also, that's moments before noita crashes.
StopDrawing()
ReleaseDC_(hWnd, windowDC)
FreeImage(imageID)
ProcedureReturn #False
EndIf
StopDrawing()
ReleaseDC_(hWnd, windowDC) *pixelBuffer = DrawingBuffer()
glReadPixels_(*capRect\left, *capRect\top, capWidth, capHeight, #GL_BGR_EXT, #GL_UNSIGNED_BYTE, *pixelBuffer)
;For y = 0 To *capRect\height - 1
; *Line.Pixel = Buffer + Pitch * y
;
; For x = 0 To *capRect\width - 1
;
; *Line\Pixel = ColorTable(pos2) ; Write the pixel directly to the memory !
; *Line+Offset
;
; ; You can try with regular plot to see the speed difference
; ; Plot(x, y, ColorTable(pos2))
; Next
; Next
StopDrawing()
LockMutex(Mutex) LockMutex(Mutex)
; Check if the queue has too many elements, if so, wait. (Emulate go's channels) ; Check if the queue has too many elements, if so, wait. (Emulate go's channels)
@ -173,13 +169,13 @@ EndProcedure
;Capture(123, 123) ;Capture(123, 123)
;Delay(1000) ;Delay(1000)
; IDE Options = PureBasic 6.00 LTS (Windows - x64) ; IDE Options = PureBasic 6.04 LTS (Windows - x64)
; ExecutableFormat = Shared dll ; ExecutableFormat = Shared dll
; CursorPosition = 94 ; CursorPosition = 126
; FirstLine = 39 ; FirstLine = 98
; Folding = -- ; Folding = -
; Optimizer ; Optimizer
; EnableThread ; EnableThread
; EnableXP ; EnableXP
; Executable = capture.dll ; Executable = capture.dll
; Compiler = PureBasic 6.00 LTS (Windows - x86) ; Compiler = PureBasic 6.04 LTS (Windows - x86)

Binary file not shown.

View File

@ -1,4 +1,4 @@
-- Copyright (c) 2019-2023 David Vogel -- Copyright (c) 2019-2024 David Vogel
-- --
-- This software is released under the MIT License. -- This software is released under the MIT License.
-- https://opensource.org/licenses/MIT -- https://opensource.org/licenses/MIT
@ -64,8 +64,8 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
end end
local rectTopLeft, rectBottomRight = ScreenCapture.GetRect() local rectTopLeft, rectBottomRight = ScreenCapture.GetRect()
if Coords.WindowResolution ~= rectBottomRight - rectTopLeft then if Coords:InternalRectSize() ~= rectBottomRight - rectTopLeft then
error(string.format("window size seems to have changed from %s to %s", Coords.WindowResolution, rectBottomRight - rectTopLeft)) error(string.format("internal rectangle size seems to have changed from %s to %s", Coords:InternalRectSize(), rectBottomRight - rectTopLeft))
end end
local topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos) local topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos)
@ -108,9 +108,13 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
-- Suspend UI drawing for 1 frame. -- Suspend UI drawing for 1 frame.
UI:SuspendDrawing(1) UI:SuspendDrawing(1)
-- First we wait one frame for the current state to be drawn.
wait(0) wait(0)
-- Fetch coordinates again, as they may have changed. -- At this point the needed frame is fully drawn, but the framebuffers are swapped.
-- Recalculate capture position and rectangle if we are not forcing any capture position.
-- We are in the `OnWorldPreUpdate` hook, this means that `CameraAPI.GetPos` return the position of the last frame.
if not pos then if not pos then
topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos) topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos)
if outputPixelScale > 0 then if outputPixelScale > 0 then
@ -120,6 +124,10 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
end end
end end
-- Wait another frame.
-- After this `wait` the framebuffer will be swapped again, and we can grab the correct frame.
wait(0)
-- The top left world position needs to be upscaled by the pixel scale. -- The top left world position needs to be upscaled by the pixel scale.
-- Otherwise it's not possible to stitch the images correctly. -- Otherwise it's not possible to stitch the images correctly.
if not ScreenCapture.Capture(topLeftCapture, bottomRightCapture, outputTopLeft, (bottomRightWorld - topLeftWorld) * outputPixelScale) then if not ScreenCapture.Capture(topLeftCapture, bottomRightCapture, outputTopLeft, (bottomRightWorld - topLeftWorld) * outputPixelScale) then

View File

@ -1,4 +1,4 @@
-- Copyright (c) 2019-2022 David Vogel -- Copyright (c) 2019-2023 David Vogel
-- --
-- This software is released under the MIT License. -- This software is released under the MIT License.
-- https://opensource.org/licenses/MIT -- https://opensource.org/licenses/MIT
@ -53,8 +53,8 @@ function Check:Regular(interval)
local topLeft, bottomRight = ScreenCap.GetRect() -- Actual window client area. local topLeft, bottomRight = ScreenCap.GetRect() -- Actual window client area.
if topLeft and bottomRight then if topLeft and bottomRight then
local actual = bottomRight - topLeft local actual = bottomRight - topLeft
if actual ~= Coords.WindowResolution then if actual ~= Coords:InternalRectSize() then
Message:ShowWrongResolution(Modification.AutoSet, string.format("Old window resolution is %s. Current resolution is %s.", Coords.WindowResolution, actual)) Message:ShowWrongResolution(Modification.AutoSet, string.format("Internal rectangle size is %s. Current resolution is %s.", Coords:InternalRectSize(), actual))
end end
else else
Message:ShowRuntimeError("GetRect", "Couldn't determine window resolution.") Message:ShowRuntimeError("GetRect", "Couldn't determine window resolution.")
@ -62,12 +62,12 @@ function Check:Regular(interval)
-- Check if we have the required settings. -- Check if we have the required settings.
local config, magic, patches = Modification.RequiredChanges() local config, magic, patches = Modification.RequiredChanges()
if config["fullscreen"] then --if config["fullscreen"] then
local expected = tonumber(config["fullscreen"]) -- local expected = tonumber(config["fullscreen"])
if expected ~= Coords.FullscreenMode then -- if expected ~= Coords.FullscreenMode then
Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Fullscreen mode %s. Expected %s.", Coords.FullscreenMode, expected)) -- Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Fullscreen mode %s. Expected %s.", Coords.FullscreenMode, expected))
end -- end
end --end
if config["window_w"] and config["window_h"] then if config["window_w"] and config["window_h"] then
local expected = Vec2(tonumber(config["window_w"]), tonumber(config["window_h"])) local expected = Vec2(tonumber(config["window_w"]), tonumber(config["window_h"]))
if expected ~= Coords.WindowResolution then if expected ~= Coords.WindowResolution then

View File

@ -1,4 +1,4 @@
-- Copyright (c) 2019-2022 David Vogel -- Copyright (c) 2019-2023 David Vogel
-- --
-- This software is released under the MIT License. -- This software is released under the MIT License.
-- https://opensource.org/licenses/MIT -- https://opensource.org/licenses/MIT
@ -24,6 +24,13 @@ ffi.cdef([[
LONG bottom; LONG bottom;
} RECT; } RECT;
typedef struct {
LONG x;
LONG y;
LONG width;
LONG height;
} GLViewportDims;
bool GetRect(RECT* rect); bool GetRect(RECT* rect);
bool Capture(RECT* rect, int x, int y, int sx, int sy); bool Capture(RECT* rect, int x, int y, int sx, int sy);
]]) ]])

View File

@ -1,4 +1,4 @@
-- Copyright (c) 2022 David Vogel -- Copyright (c) 2022-2024 David Vogel
-- --
-- This software is released under the MIT License. -- This software is released under the MIT License.
-- https://opensource.org/licenses/MIT -- https://opensource.org/licenses/MIT
@ -108,7 +108,6 @@ end
---Called *every* time the game is about to start updating the world. ---Called *every* time the game is about to start updating the world.
function OnWorldPreUpdate() function OnWorldPreUpdate()
Message:CatchException("OnWorldPreUpdate", function() Message:CatchException("OnWorldPreUpdate", function()
-- Coroutines aren't run every frame in this lua sandbox, do it manually here. -- Coroutines aren't run every frame in this lua sandbox, do it manually here.
wake_up_waiting_threads(1) wake_up_waiting_threads(1)