mcap/modules/Screen_Record/capture_gpu.jai
Musa Mahmood 93274a2b04 [0.1.5] Video capture phase 3: Ctrl+PrintScreen records a region into the library
- record_region, a new [global_hotkeys] action on Ctrl-PrintScreen (Windows only; none on
  macOS): starts a recording, or stops the one running.
- The overlay's video mode (screen_capture_win32.jai): red outline and magnifier; the drag held
  to the screen it began on; under 240 x 180 (record_size_allowed, in Screen_Record) the outline
  is dashed, the label gives the minimum, and letting go takes nothing; a click or Enter takes
  the whole screen; no pixels copied. With immediate, no overlay at all: the screen under the
  cursor, for PrintScreen while recording.
- src/app/screen_record.jai: records with modules/Screen_Record to %TEMP%/mcap at 30 fps; a
  recording that stops by itself keeps what it has, as does quitting (the shutdown step stops it).
  The MP4 goes into the library through the stills' jobs: a video .mcap in Screen Captures, the
  MP4 its original, a frame of it its rendered image, metadata.json as a still's, capture.json
  with Duration, FrameRate, VideoCodec, PosterTime, Encoder, Capture and DroppedFrames; the MP4 is
  deleted once it is in. The catalog reads a video's keys from capture.json too.
- The capture thread makes itself aware of each monitor's DPI: ImGui makes only the main thread
  aware, and Desktop Duplication refused the app's capture thread with DXGI_ERROR_UNSUPPORTED
  (the fallback recorded with Windows.Graphics.Capture, and the log said so).
- -record SECONDS records the screen under the cursor, puts it in the library and exits;
  -record-quit quits while recording instead. Both pass on a scratch library.
- Tests: a "screen recording" group (the size rule; an MP4 from the module's encoder written as a
  recording reads back, and the catalog takes it as a video). The segment overlay test no longer
  depends on the colours chosen in the app's own data folder.
- README, the format's README and the plan describe it.
2026-10-10 07:14:35 +01:00

116 lines
4.7 KiB
Plaintext

// What both ways of taking the screen share (dxgi_source.jai, wgc_source.jai): a D3D11 device of
// their own on the adapter the screen is attached to, the region as last seen, copied out of each
// new image on the GPU, and the staging textures it is read back through.
Capture_Gpu :: struct {
adapter: *IDXGIAdapter1;
device: *ID3D11Device;
device_context: *ID3D11DeviceContext;
box: D3D11_BOX; // the region, in the screen's pixels
latest: *ID3D11Texture2D; // the region as last seen
staging: [RECORD_STAGING] *ID3D11Texture2D;
copy_ticks: s64; // spent taking the region out of images (QueryPerformanceCounter ticks)
}
RECORD_STAGING :: 3; // staging textures in turn: the one read is two frames behind the one copied
// What a source's next() found.
Capture_Next :: enum u8 { NONE; IMAGE; POINTER; IMAGE_AND_POINTER; LOST; FAILED; }
// The device on the screen's adapter, and the textures for a region (in the screen's pixels).
gpu_open :: (g: *Capture_Gpu, screen: Record_Screen, region: RECT) -> ok: bool, error: string {
factory: *IDXGIFactory1;
hr := CreateDXGIFactory1(*uid(IDXGIFactory1_UUID), xx *factory);
if FAILED(hr) return false, hresult_text("CreateDXGIFactory1", hr);
defer IUnknown_Release(factory);
hr = IDXGIFactory1_EnumAdapters1(factory, screen.adapter_index, *g.adapter);
if FAILED(hr) return false, hresult_text("the screen's adapter is gone", hr);
levels := D3D_FEATURE_LEVEL.[._11_1, ._11_0, ._10_1, ._10_0];
level: D3D_FEATURE_LEVEL;
hr = D3D11CreateDevice(g.adapter, .UNKNOWN, null, .BGRA_SUPPORT, levels.data, levels.count, D3D11_SDK_VERSION, *g.device, *level, *g.device_context);
if FAILED(hr) return false, hresult_text("D3D11CreateDevice on the screen's adapter", hr);
g.box = .{left = xx region.left, top = xx region.top, right = xx region.right, bottom = xx region.bottom, front = 0, back = 1};
desc: D3D11_TEXTURE2D_DESC;
desc.Width = cast(u32) (region.right - region.left);
desc.Height = cast(u32) (region.bottom - region.top);
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = .B8G8R8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.Usage = .DEFAULT;
hr = ID3D11Device_CreateTexture2D(g.device, *desc, null, *g.latest);
if FAILED(hr) return false, hresult_text("a texture for the region", hr);
desc.Usage = .STAGING;
desc.CPUAccessFlags = .READ;
for * g.staging {
hr = ID3D11Device_CreateTexture2D(g.device, *desc, null, it);
if FAILED(hr) return false, hresult_text("a staging texture", hr);
}
return true, "";
}
// The region of a whole screen's image into g.latest.
gpu_copy_region :: (g: *Capture_Gpu, image: *ID3D11Texture2D) {
start := ticks_now();
ID3D11DeviceContext_CopySubresourceRegion(g.device_context, g.latest, 0, 0, 0, 0, image, 0, *g.box);
g.copy_ticks += ticks_now() - start;
}
gpu_close :: (g: *Capture_Gpu) {
for * g.staging com_release(it);
com_release(*g.latest);
com_release(*g.device_context);
com_release(*g.device);
com_release(*g.adapter);
}
// The calling thread made aware of each monitor's DPI (Windows 10 1703 on), so the desktop it is told
// of is in the screens' own pixels.
gpu_thread_dpi_aware :: () {
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 :: cast(HANDLE) -4;
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
}
#scope_module
user32_dll :: #library,system "user32";
SetThreadDpiAwarenessContext :: (value: HANDLE) -> HANDLE #foreign user32_dll;
com_release :: (p: *$T) {
if p.* { IUnknown_Release(cast(*IUnknown) p.*); p.* = null; }
}
ticks_now :: () -> s64 {
t: LARGE_INTEGER;
QueryPerformanceCounter(*t);
return t.QuadPart;
}
hresult_is :: (hr: HRESULT, code: u32) -> bool {
return cast,no_check(u32) hr == code;
}
// "what failed: 0x887A0004 (DXGI_ERROR_UNSUPPORTED)", temp.
hresult_text :: (what: string, hr: HRESULT) -> string {
code := cast,no_check(u32) hr;
name := "";
if code == {
case DXGI_ERROR_UNSUPPORTED; name = " (DXGI_ERROR_UNSUPPORTED)";
case DXGI_ERROR_NOT_CURRENTLY_AVAILABLE; name = " (DXGI_ERROR_NOT_CURRENTLY_AVAILABLE)";
case DXGI_ERROR_ACCESS_LOST; name = " (DXGI_ERROR_ACCESS_LOST)";
case 0x80070005; name = " (E_ACCESSDENIED)";
case 0x80004002; name = " (E_NOINTERFACE)";
case 0x80070057; name = " (E_INVALIDARG)";
case 0x80040154; name = " (REGDB_E_CLASSNOTREG)";
case 0x80004001; name = " (E_NOTIMPL)";
}
return tprint("%: 0x%%", what, formatInt(code, base = 16, minimum_digits = 8), name);
}
wide_text :: (wide: *s16) -> string {
text, ok := wide_to_utf8(cast(*u16) wide,, temp);
return ifx ok then text else "";
}