Capture directly from OpenGL framebuffer

- Update capture.dll to read via glReadPixels
- Move coroutine wake up into OnWorldPostUpdate
- Update resolution checks for new capturing method
- Remove fullscreen mode check
- Increase screen capture delay
This commit is contained in:
David Vogel 2024-01-04 19:36:36 +01:00
parent 4de83e3dcd
commit d5cd88a30e
6 changed files with 93 additions and 82 deletions

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.
; https://opensource.org/licenses/MIT
@ -15,44 +15,37 @@ Structure QueueElement
sy.i
EndStructure
; Source: https://www.purebasic.fr/english/viewtopic.php?f=13&t=29981&start=15
Procedure EnumWindowsProc(hWnd.l, *lParam.Long)
Protected lpProc.l
GetWindowThreadProcessId_(hWnd, @lpProc)
If *lParam\l = lpProc ; Check if current window's processID matches
*lParam\l = hWnd ; Replace processID in the param With the hwnd As result
ProcedureReturn #False ; Return false to stop iterating
Structure GLViewportDims
x.i
y.i
width.i
height.i
EndStructure
; Returns the size of the main OpenGL rendering output.
ProcedureDLL GetGLViewportSize(*dims.GLViewportDims)
If Not *dims
ProcedureReturn #False
EndIf
glGetIntegerv_(#GL_VIEWPORT, *dims)
ProcedureReturn #True
EndProcedure
; Source: https://www.purebasic.fr/english/viewtopic.php?f=13&t=29981&start=15
; 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
; Returns the size of the main OpenGL rendering output as a windows RECT.
ProcedureDLL GetRect(*rect.RECT)
Protected hWnd.l = GetProcHwnd()
If Not hWnd
ProcedureReturn #False
EndIf
If Not *rect
ProcedureReturn #False
EndIf
GetClientRect_(hWnd, *rect)
Protected dims.GLViewportDims
glGetIntegerv_(#GL_VIEWPORT, dims)
; A RECT consists basically of two POINT structures
ClientToScreen_(hWnd, @*rect\left)
ClientToScreen_(hWnd, @*rect\Right)
*rect\left = dims\x
*rect\top = dims\y
*rect\right = dims\x + dims\width
*rect\bottom = dims\y + dims\height
ProcedureReturn #True
EndProcedure
@ -97,53 +90,56 @@ Procedure Worker(*Dummy)
EndProcedure
; 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.
; 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)
Protected hWnd.l = GetProcHwnd()
If Not hWnd
Protected viewportRect.RECT
If Not GetRect(@viewportRect)
ProcedureReturn #False
EndIf
Protected rect.RECT
If Not GetRect(@rect)
ProcedureReturn #False
EndIf
; Limit the desired capture area to the actual client area of the window.
; Limit the desired capture area to the actual client area of the viewport.
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\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
ProcedureReturn #False
EndIf
; Get DC of window.
windowDC = GetDC_(hWnd)
If Not windowDC
FreeImage(imageID)
ProcedureReturn #False
EndIf
;Protected *pixelBuf = AllocateMemory(3 * width * height)
hDC = StartDrawing(ImageOutput(imageID))
If Not hDC
ReleaseDC_(hWnd, windowDC)
FreeImage(imageID)
ProcedureReturn #False
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)
; Check if the queue has too many elements, if so, wait. (Emulate go's channels)
@ -173,13 +169,13 @@ EndProcedure
;Capture(123, 123)
;Delay(1000)
; IDE Options = PureBasic 6.00 LTS (Windows - x64)
; IDE Options = PureBasic 6.04 LTS (Windows - x64)
; ExecutableFormat = Shared dll
; CursorPosition = 94
; FirstLine = 39
; Folding = --
; CursorPosition = 126
; FirstLine = 98
; Folding = -
; Optimizer
; EnableThread
; EnableXP
; Executable = capture.dll
; Compiler = PureBasic 6.00 LTS (Windows - x86)
; Compiler = PureBasic 6.04 LTS (Windows - x86)

Binary file not shown.

View File

@ -64,8 +64,8 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
end
local rectTopLeft, rectBottomRight = ScreenCapture.GetRect()
if Coords.WindowResolution ~= rectBottomRight - rectTopLeft then
error(string.format("window size seems to have changed from %s to %s", Coords.WindowResolution, rectBottomRight - rectTopLeft))
if Coords:InternalRectSize() ~= rectBottomRight - rectTopLeft then
error(string.format("internal rectangle size seems to have changed from %s to %s", Coords:InternalRectSize(), rectBottomRight - rectTopLeft))
end
local topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos)
@ -108,9 +108,9 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
-- Suspend UI drawing for 1 frame.
UI:SuspendDrawing(1)
wait(0)
--wait(0)
-- Fetch coordinates again, as they may have changed.
-- Recalculate capture position and rectangle if we are not forcing any capture position.
if not pos then
topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos)
if outputPixelScale > 0 then
@ -120,6 +120,9 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
end
end
-- Wait for two frames.
wait(1)
-- The top left world position needs to be upscaled by the pixel scale.
-- Otherwise it's not possible to stitch the images correctly.
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.
-- https://opensource.org/licenses/MIT
@ -53,8 +53,8 @@ function Check:Regular(interval)
local topLeft, bottomRight = ScreenCap.GetRect() -- Actual window client area.
if topLeft and bottomRight then
local actual = bottomRight - topLeft
if actual ~= Coords.WindowResolution then
Message:ShowWrongResolution(Modification.AutoSet, string.format("Old window resolution is %s. Current resolution is %s.", Coords.WindowResolution, actual))
if actual ~= Coords:InternalRectSize() then
Message:ShowWrongResolution(Modification.AutoSet, string.format("Internal rectangle size is %s. Current resolution is %s.", Coords:InternalRectSize(), actual))
end
else
Message:ShowRuntimeError("GetRect", "Couldn't determine window resolution.")
@ -62,12 +62,12 @@ function Check:Regular(interval)
-- Check if we have the required settings.
local config, magic, patches = Modification.RequiredChanges()
if config["fullscreen"] then
local expected = tonumber(config["fullscreen"])
if expected ~= Coords.FullscreenMode then
Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Fullscreen mode %s. Expected %s.", Coords.FullscreenMode, expected))
end
end
--if config["fullscreen"] then
-- local expected = tonumber(config["fullscreen"])
-- if expected ~= Coords.FullscreenMode then
-- Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Fullscreen mode %s. Expected %s.", Coords.FullscreenMode, expected))
-- end
--end
if config["window_w"] and config["window_h"] then
local expected = Vec2(tonumber(config["window_w"]), tonumber(config["window_h"]))
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.
-- https://opensource.org/licenses/MIT
@ -24,6 +24,13 @@ ffi.cdef([[
LONG bottom;
} RECT;
typedef struct {
LONG x;
LONG y;
LONG width;
LONG height;
} GLViewportDims;
bool GetRect(RECT* rect);
bool Capture(RECT* rect, int x, int y, int sx, int sy);
]])

View File

@ -108,15 +108,20 @@ end
---Called *every* time the game is about to start updating the world.
function OnWorldPreUpdate()
Message:CatchException("OnWorldPreUpdate", function()
-- Coroutines aren't run every frame in this lua sandbox, do it manually here.
wake_up_waiting_threads(1)
--wake_up_waiting_threads(1)
end)
end
---Called *every* time the game has finished updating the world.
function OnWorldPostUpdate()
Message:CatchException("OnWorldPreUpdate", function()
-- Coroutines aren't run every frame in this lua sandbox, do it manually here.
wake_up_waiting_threads(1)
end)
Message:CatchException("OnWorldPostUpdate", function()
-- Reload mod every 60 frames.
-- This allows live updates to the mod while Noita is running.