Skip to content

Commit d5cd88a

Browse files
committed
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
1 parent 4de83e3 commit d5cd88a

File tree

6 files changed

+93
-82
lines changed

6 files changed

+93
-82
lines changed

bin/capture-b/Capture.pb

+62-66
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
; Copyright (c) 2019-2022 David Vogel
1+
; Copyright (c) 2019-2023 David Vogel
22
;
33
; This software is released under the MIT License.
44
; https://opensource.org/licenses/MIT
@@ -15,44 +15,37 @@ Structure QueueElement
1515
sy.i
1616
EndStructure
1717

18-
; Source: https://www.purebasic.fr/english/viewtopic.php?f=13&t=29981&start=15
19-
Procedure EnumWindowsProc(hWnd.l, *lParam.Long)
20-
Protected lpProc.l
21-
GetWindowThreadProcessId_(hWnd, @lpProc)
22-
If *lParam\l = lpProc ; Check if current window's processID matches
23-
*lParam\l = hWnd ; Replace processID in the param With the hwnd As result
24-
ProcedureReturn #False ; Return false to stop iterating
25-
EndIf
26-
ProcedureReturn #True
27-
EndProcedure
18+
Structure GLViewportDims
19+
x.i
20+
y.i
21+
width.i
22+
height.i
23+
EndStructure
2824

29-
; Source: https://www.purebasic.fr/english/viewtopic.php?f=13&t=29981&start=15
30-
; Returns the first window associated with the given process handle
31-
Procedure GetProcHwnd()
32-
Protected pID.l = GetCurrentProcessId_()
33-
Protected tempParam.l = pID
34-
EnumWindows_(@EnumWindowsProc(), @tempParam)
35-
If tempParam = pID ; Check if anything was found
36-
ProcedureReturn #Null
25+
; Returns the size of the main OpenGL rendering output.
26+
ProcedureDLL GetGLViewportSize(*dims.GLViewportDims)
27+
If Not *dims
28+
ProcedureReturn #False
3729
EndIf
38-
ProcedureReturn tempParam ; This is a valid hWnd at this point
30+
31+
glGetIntegerv_(#GL_VIEWPORT, *dims)
32+
33+
ProcedureReturn #True
3934
EndProcedure
4035

41-
; Get the client rectangle of the "Main" window of this process in screen coordinates
36+
; Returns the size of the main OpenGL rendering output as a windows RECT.
4237
ProcedureDLL GetRect(*rect.RECT)
43-
Protected hWnd.l = GetProcHwnd()
44-
If Not hWnd
45-
ProcedureReturn #False
46-
EndIf
4738
If Not *rect
4839
ProcedureReturn #False
4940
EndIf
50-
51-
GetClientRect_(hWnd, *rect)
52-
53-
; A RECT consists basically of two POINT structures
54-
ClientToScreen_(hWnd, @*rect\left)
55-
ClientToScreen_(hWnd, @*rect\Right)
41+
42+
Protected dims.GLViewportDims
43+
glGetIntegerv_(#GL_VIEWPORT, dims)
44+
45+
*rect\left = dims\x
46+
*rect\top = dims\y
47+
*rect\right = dims\x + dims\width
48+
*rect\bottom = dims\y + dims\height
5649

5750
ProcedureReturn #True
5851
EndProcedure
@@ -84,7 +77,7 @@ Procedure Worker(*Dummy)
8477
sy = Queue()\sy
8578
DeleteElement(Queue())
8679
UnlockMutex(Mutex)
87-
80+
8881
If sx > 0 And sy > 0
8982
ResizeImage(img, sx, sy)
9083
EndIf
@@ -97,53 +90,56 @@ Procedure Worker(*Dummy)
9790
EndProcedure
9891

9992
; Takes a screenshot of the client area of this process' active window.
100-
; The portion of the client area that is captured is described by capRect, which is in window coordinates and relative to the client area.
93+
; The portion of the client area that is captured is described by capRect, which is in viewport coordinates.
10194
; 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.
10295
; sx and sy defines the final dimensions that the screenshot will be resized to. No resize will happen if set to 0.
10396
ProcedureDLL Capture(*capRect.RECT, x.l, y.l, sx.l, sy.l)
104-
Protected hWnd.l = GetProcHwnd()
105-
If Not hWnd
97+
Protected viewportRect.RECT
98+
If Not GetRect(@viewportRect)
10699
ProcedureReturn #False
107100
EndIf
108101

109-
Protected rect.RECT
110-
If Not GetRect(@rect)
111-
ProcedureReturn #False
112-
EndIf
113-
114-
; Limit the desired capture area to the actual client area of the window.
102+
; Limit the desired capture area to the actual client area of the viewport.
115103
If *capRect\left < 0 : *capRect\left = 0 : EndIf
116-
If *capRect\right > rect\right-rect\left : *capRect\right = rect\right-rect\left : EndIf
117104
If *capRect\top < 0 : *capRect\top = 0 : EndIf
118-
If *capRect\bottom > rect\bottom-rect\top : *capRect\bottom = rect\bottom-rect\top : EndIf
119-
120-
imageID = CreateImage(#PB_Any, *capRect\right-*capRect\left, *capRect\bottom-*capRect\top)
105+
If *capRect\right < *capRect\left : *capRect\right = *capRect\left : EndIf
106+
If *capRect\bottom < *capRect\top : *capRect\bottom = *capRect\top : EndIf
107+
If *capRect\right > viewportRect\right : *capRect\right = viewportRect\right : EndIf
108+
If *capRect\bottom > viewportRect\bottom : *capRect\bottom = viewportRect\bottom : EndIf
109+
110+
Protected capWidth = *capRect\right - *capRect\left
111+
Protected capHeight = *capRect\bottom - *capRect\top
112+
113+
imageID = CreateImage(#PB_Any, capWidth, capHeight)
121114
If Not imageID
122115
ProcedureReturn #False
123116
EndIf
124-
125-
; Get DC of window.
126-
windowDC = GetDC_(hWnd)
127-
If Not windowDC
128-
FreeImage(imageID)
129-
ProcedureReturn #False
130-
EndIf
117+
118+
;Protected *pixelBuf = AllocateMemory(3 * width * height)
131119

132120
hDC = StartDrawing(ImageOutput(imageID))
133121
If Not hDC
134-
ReleaseDC_(hWnd, windowDC)
135122
FreeImage(imageID)
136123
ProcedureReturn #False
137124
EndIf
138-
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.
139-
StopDrawing()
140-
ReleaseDC_(hWnd, windowDC)
141-
FreeImage(imageID)
142-
ProcedureReturn #False
143-
EndIf
144-
StopDrawing()
125+
126+
*pixelBuffer = DrawingBuffer()
127+
glReadPixels_(*capRect\left, *capRect\top, capWidth, capHeight, #GL_BGR_EXT, #GL_UNSIGNED_BYTE, *pixelBuffer)
128+
129+
;For y = 0 To *capRect\height - 1
130+
; *Line.Pixel = Buffer + Pitch * y
131+
;
132+
; For x = 0 To *capRect\width - 1
133+
;
134+
; *Line\Pixel = ColorTable(pos2) ; Write the pixel directly to the memory !
135+
; *Line+Offset
136+
;
137+
; ; You can try with regular plot to see the speed difference
138+
; ; Plot(x, y, ColorTable(pos2))
139+
; Next
140+
; Next
145141

146-
ReleaseDC_(hWnd, windowDC)
142+
StopDrawing()
147143

148144
LockMutex(Mutex)
149145
; Check if the queue has too many elements, if so, wait. (Emulate go's channels)
@@ -173,13 +169,13 @@ EndProcedure
173169
;Capture(123, 123)
174170
;Delay(1000)
175171

176-
; IDE Options = PureBasic 6.00 LTS (Windows - x64)
172+
; IDE Options = PureBasic 6.04 LTS (Windows - x64)
177173
; ExecutableFormat = Shared dll
178-
; CursorPosition = 94
179-
; FirstLine = 39
180-
; Folding = --
174+
; CursorPosition = 126
175+
; FirstLine = 98
176+
; Folding = -
181177
; Optimizer
182178
; EnableThread
183179
; EnableXP
184180
; Executable = capture.dll
185-
; Compiler = PureBasic 6.00 LTS (Windows - x86)
181+
; Compiler = PureBasic 6.04 LTS (Windows - x86)

bin/capture-b/capture.dll

15 KB
Binary file not shown.

files/capture.lua

+7-4
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
6464
end
6565

6666
local rectTopLeft, rectBottomRight = ScreenCapture.GetRect()
67-
if Coords.WindowResolution ~= rectBottomRight - rectTopLeft then
68-
error(string.format("window size seems to have changed from %s to %s", Coords.WindowResolution, rectBottomRight - rectTopLeft))
67+
if Coords:InternalRectSize() ~= rectBottomRight - rectTopLeft then
68+
error(string.format("internal rectangle size seems to have changed from %s to %s", Coords:InternalRectSize(), rectBottomRight - rectTopLeft))
6969
end
7070

7171
local topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos)
@@ -108,9 +108,9 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
108108
-- Suspend UI drawing for 1 frame.
109109
UI:SuspendDrawing(1)
110110

111-
wait(0)
111+
--wait(0)
112112

113-
-- Fetch coordinates again, as they may have changed.
113+
-- Recalculate capture position and rectangle if we are not forcing any capture position.
114114
if not pos then
115115
topLeftCapture, bottomRightCapture, topLeftWorld, bottomRightWorld = calculateCaptureRectangle(pos)
116116
if outputPixelScale > 0 then
@@ -120,6 +120,9 @@ local function captureScreenshot(pos, ensureLoaded, dontOverwrite, ctx, outputPi
120120
end
121121
end
122122

123+
-- Wait for two frames.
124+
wait(1)
125+
123126
-- The top left world position needs to be upscaled by the pixel scale.
124127
-- Otherwise it's not possible to stitch the images correctly.
125128
if not ScreenCapture.Capture(topLeftCapture, bottomRightCapture, outputTopLeft, (bottomRightWorld - topLeftWorld) * outputPixelScale) then

files/check.lua

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
-- Copyright (c) 2019-2022 David Vogel
1+
-- Copyright (c) 2019-2023 David Vogel
22
--
33
-- This software is released under the MIT License.
44
-- https://opensource.org/licenses/MIT
@@ -53,21 +53,21 @@ function Check:Regular(interval)
5353
local topLeft, bottomRight = ScreenCap.GetRect() -- Actual window client area.
5454
if topLeft and bottomRight then
5555
local actual = bottomRight - topLeft
56-
if actual ~= Coords.WindowResolution then
57-
Message:ShowWrongResolution(Modification.AutoSet, string.format("Old window resolution is %s. Current resolution is %s.", Coords.WindowResolution, actual))
56+
if actual ~= Coords:InternalRectSize() then
57+
Message:ShowWrongResolution(Modification.AutoSet, string.format("Internal rectangle size is %s. Current resolution is %s.", Coords:InternalRectSize(), actual))
5858
end
5959
else
6060
Message:ShowRuntimeError("GetRect", "Couldn't determine window resolution.")
6161
end
6262

6363
-- Check if we have the required settings.
6464
local config, magic, patches = Modification.RequiredChanges()
65-
if config["fullscreen"] then
66-
local expected = tonumber(config["fullscreen"])
67-
if expected ~= Coords.FullscreenMode then
68-
Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Fullscreen mode %s. Expected %s.", Coords.FullscreenMode, expected))
69-
end
70-
end
65+
--if config["fullscreen"] then
66+
-- local expected = tonumber(config["fullscreen"])
67+
-- if expected ~= Coords.FullscreenMode then
68+
-- Message:ShowSetNoitaSettings(Modification.AutoSet, string.format("Fullscreen mode %s. Expected %s.", Coords.FullscreenMode, expected))
69+
-- end
70+
--end
7171
if config["window_w"] and config["window_h"] then
7272
local expected = Vec2(tonumber(config["window_w"]), tonumber(config["window_h"]))
7373
if expected ~= Coords.WindowResolution then

files/libraries/screen-capture.lua

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
-- Copyright (c) 2019-2022 David Vogel
1+
-- Copyright (c) 2019-2023 David Vogel
22
--
33
-- This software is released under the MIT License.
44
-- https://opensource.org/licenses/MIT
@@ -24,6 +24,13 @@ ffi.cdef([[
2424
LONG bottom;
2525
} RECT;
2626
27+
typedef struct {
28+
LONG x;
29+
LONG y;
30+
LONG width;
31+
LONG height;
32+
} GLViewportDims;
33+
2734
bool GetRect(RECT* rect);
2835
bool Capture(RECT* rect, int x, int y, int sx, int sy);
2936
]])

init.lua

+7-2
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,20 @@ end
108108
---Called *every* time the game is about to start updating the world.
109109
function OnWorldPreUpdate()
110110
Message:CatchException("OnWorldPreUpdate", function()
111-
112111
-- Coroutines aren't run every frame in this lua sandbox, do it manually here.
113-
wake_up_waiting_threads(1)
112+
--wake_up_waiting_threads(1)
114113

115114
end)
116115
end
117116

118117
---Called *every* time the game has finished updating the world.
119118
function OnWorldPostUpdate()
119+
Message:CatchException("OnWorldPreUpdate", function()
120+
-- Coroutines aren't run every frame in this lua sandbox, do it manually here.
121+
wake_up_waiting_threads(1)
122+
123+
end)
124+
120125
Message:CatchException("OnWorldPostUpdate", function()
121126
-- Reload mod every 60 frames.
122127
-- This allows live updates to the mod while Noita is running.

0 commit comments

Comments
 (0)