Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bb0a8e7
NeoPic: allow loading an owned capture Surface without cloning
defisym Nov 6, 2026
d8076cb
NeoPic: test client-area crop mapping across scale and monitor coordi…
defisym Nov 6, 2026
95756ff
NeoPic: add opt-in Secondary Surface and WGC capture experiments
defisym Nov 6, 2026
e44b1f9
NeoPic: gate capture publication and WGC frame release on GPU completion
defisym Nov 6, 2026
000642a
NeoPic: capture synchronously through Fusion Surface geometry
defisym Nov 11, 2026
286e093
NeoPic: diagnose WGC startup failures and document capture limits
defisym Nov 11, 2026
620fef6
NeoPic: fix RGBAREF narrowing and isolate capture backend compilation
defisym Nov 11, 2026
6ca7aa3
NeoPic: add opaque RGB normalization pass and WARP pixel regression
defisym Nov 11, 2026
11d29d1
NeoPic: output capture in scene units and document WGC border
defisym Nov 11, 2026
4588e56
NeoPic: check capture backends and normalization on Windows
defisym Nov 11, 2026
70d3c67
NeoPic: scope public Windows checks without proprietary Fusion SDK
defisym Nov 11, 2026
9ac7451
NeoPic: add deterministic capture benchmark timing and statistics
defisym Nov 11, 2026
2324cf2
NeoPic: benchmark capture and _TempCapture handoff inside the action
defisym Nov 11, 2026
2eb1aad
NeoPic: check benchmark configurations and statistics on Windows
defisym Nov 11, 2026
2780a10
NeoPic: compile benchmark instrumentation out behind a disabled maste…
defisym Nov 11, 2026
41afcbd
NeoPic: diagnose conditional secondary sources and document blank com…
defisym Nov 11, 2026
67b06fa
NeoPic: stretch independent capture source dimensions to output like DC
defisym Nov 11, 2026
bbc3acd
NeoPic: select capture backends per object and isolate benchmark subc…
defisym Nov 12, 2026
a26885d
NeoPic: verify capture inheritance and edit-data migration
defisym Nov 12, 2026
84c66de
NeoPic: name capture resources and check real D3D debug-layer lifetimes
defisym Nov 12, 2026
e0ce5e4
NeoPic: avoid a shared ComPtr alias collision in the lifetime test
defisym Nov 12, 2026
6337af2
NeoPic: explicitly release native capture caches and trace teardown
defisym Nov 12, 2026
d2f140f
NeoPic: fall back to DC for DX11 capture choices on Fusion DX9
defisym Nov 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/neopic-capture-compile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: NeoPic capture Windows checks
on:
pull_request:
paths:
- 'Extensions/NeoPic/**'
- 'Extensions/_DeLib/**'
- 'Extensions/_3rdLib/D3DUtilities/**'
- 'Inc/**'
- '.github/workflows/neopic-capture-compile.yml'
permissions:
contents: read
jobs:
capture:
runs-on: windows-2022
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Check WGC, GPU pixels/lifetime, inheritance and edit-data migration
shell: pwsh
run: |
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$vs = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
if (-not $vs) { throw 'VS C++ tools not found' }
Import-Module "$vs\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"
Enter-VsDevShell -VsInstallPath $vs -SkipAutomaticLocation -DevCmdArguments '-arch=x86 -host_arch=x64'
./Extensions/NeoPic/tests/CompileCapture.ps1 -NativeOnly -RunGpuSmokeTest
14 changes: 14 additions & 0 deletions Extensions/NeoPic/CaptureBackend.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#pragma once
#include <cstdint>

// Stable property values, also used by the runtime factory and diagnostics.
enum class CaptureBackend : std::uint32_t {
Dc = 0,
Secondary = 1,
WindowsGraphicsCapture = 2,
};

constexpr CaptureBackend NormalizeCaptureBackend(std::uint32_t value) {
return value <= static_cast<std::uint32_t>(CaptureBackend::WindowsGraphicsCapture)
? static_cast<CaptureBackend>(value) : CaptureBackend::Dc;
}
66 changes: 66 additions & 0 deletions Extensions/NeoPic/CaptureBenchmark.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#pragma once
#include <algorithm>
#include <cstdint>
#include <numeric>
#include <stdexcept>
#include <vector>
#include <utility>

namespace neopic_capture {
struct TimingStats {
std::size_t count = 0;
double mean = 0, minimum = 0, median = 0, p95 = 0, maximum = 0;
};
inline TimingStats Summarize(std::vector<double> values) {
TimingStats s;
s.count = values.size();
if (values.empty()) return s;
std::sort(values.begin(), values.end());
s.minimum = values.front(); s.maximum = values.back();
s.mean = std::accumulate(values.begin(), values.end(), 0.0) / static_cast<double>(values.size());
s.median = values.size() % 2 ? values[values.size() / 2]
: (values[values.size() / 2 - 1] + values[values.size() / 2]) / 2;
s.p95 = values[(values.size() * 95 + 99) / 100 - 1]; // nearest rank
return s;
}
struct BenchmarkResult {
TimingStats success, failure;
std::size_t warmupSuccess = 0, warmupFailure = 0;
double firstMs = 0, totalMs = 0;
bool firstSuccess = false, noReadySource = false;
};

// The callback is the whole Capture + _TempCapture handoff. The clock supplies
// monotonic ticks; per-iteration output and file I/O belong outside this runner.
template<class Capture, class Clock>
BenchmarkResult RunBenchmark(std::size_t iterations, std::size_t warmup,
Capture&& capture, Clock&& now, std::int64_t ticksPerSecond) {
if (!iterations || ticksPerSecond <= 0) throw std::invalid_argument("benchmark count/timer");
BenchmarkResult r;
std::vector<double> good, bad;
good.reserve(iterations); bad.reserve(iterations);
auto ms = [ticksPerSecond](auto delta) { return static_cast<double>(delta) * 1000.0 / static_cast<double>(ticksPerSecond); };
for (std::size_t i = 0; i < warmup; ++i) {
const auto start = now();
const bool ok = capture();
const auto stop = now();
if (i == 0) { r.firstMs = ms(stop - start); r.firstSuccess = ok; }
if (ok) ++r.warmupSuccess; else ++r.warmupFailure;
}
// E_PENDING must not become an impressive-looking fast benchmark result.
if (warmup && !r.warmupSuccess) { r.noReadySource = true; return r; }
const auto batchStart = now();
for (std::size_t i = 0; i < iterations; ++i) {
const auto start = now();
const bool ok = capture();
const auto stop = now();
const double elapsed = ms(stop - start);
if (!warmup && i == 0) { r.firstMs = elapsed; r.firstSuccess = ok; }
(ok ? good : bad).push_back(elapsed);
}
r.totalMs = ms(now() - batchStart);
r.success = Summarize(std::move(good));
r.failure = Summarize(std::move(bad));
return r;
}
}
237 changes: 237 additions & 0 deletions Extensions/NeoPic/CaptureBlit.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
#pragma once
#include <d3d11_1.h>
#include <d3d11sdklayers.h>
#include <wrl/client.h>
#include <tuple>
#include <cstring>
#include "D3DUtilities/ShaderCompiler.h"
#pragma comment(lib, "dxguid.lib")

// Normalize a native RGB capture into a Fusion RTT: crop valid source pixels,
// resize to scene coordinates, convert channel layout and write opaque alpha.
// Reuse the repository shader compiler; isolate all changed pipeline state.
class CaptureBlit {
template<class T> using Ptr = Microsoft::WRL::ComPtr<T>;
Ptr<ID3D11Device> owner;
Ptr<ID3DDeviceContextState> scratch;
Ptr<ID3D11VertexShader> vertex;
Ptr<ID3D11PixelShader> pixel;
Ptr<ID3D11SamplerState> pointSampler, linearSampler;
Ptr<ID3D11RasterizerState> rasterizer;
Ptr<ID3D11BlendState> blend;
Ptr<ID3D11DepthStencilState> depth;
Ptr<ID3D11Texture2D> sampleTexture, resolved;
Ptr<ID3D11ShaderResourceView> sampleView;
bool debugNames = false;

void Name(ID3D11DeviceChild* object, const char* value) const {
if (debugNames && object)
object->SetPrivateData(WKPDID_D3DDebugObjectName, static_cast<UINT>(std::strlen(value)), value);
}

HRESULT Initialize(ID3D11Device* device) {
if (owner.Get() == device && scratch) return S_OK;
Reset();
{
Ptr<ID3D11Debug> debug;
debugNames = SUCCEEDED(device->QueryInterface(IID_PPV_ARGS(debug.GetAddressOf())));
}
Ptr<ID3D11Device1> device1;
HRESULT hr = device->QueryInterface(IID_PPV_ARGS(device1.GetAddressOf()));
if (FAILED(hr)) return hr;
const D3D_FEATURE_LEVEL level = device->GetFeatureLevel();
D3D_FEATURE_LEVEL selected{};
Ptr<ID3DDeviceContextState> nextState;
hr = device1->CreateDeviceContextState(0, &level, 1, D3D11_SDK_VERSION,
__uuidof(ID3D11Device), &selected, nextState.GetAddressOf());
if (FAILED(hr)) return hr;
Name(nextState.Get(), "NeoPic.Capture.ContextState");
constexpr const char* code = R"(
struct V { float4 p : SV_Position; float2 uv : TEXCOORD0; };
V VS(uint id : SV_VertexID) {
V v; float2 uv = float2((id << 1) & 2, id & 2);
v.p = float4(uv.x * 2 - 1, 1 - uv.y * 2, 0, 1); v.uv = uv; return v;
}
Texture2D<float4> image : register(t0);
SamplerState imageSampler : register(s0);
float4 PS(V v) : SV_Target { return float4(image.Sample(imageSampler, v.uv).rgb, 1); }
)";
ShaderCompiler compiler(device);
ShaderCompiler::Blob vs, ps;
if (!compiler.CompileShader(code, std::strlen(code), "VS", "vs_4_0", vs)
|| !compiler.CompileShader(code, std::strlen(code), "PS", "ps_4_0", ps)) {
OutputDebugStringA(compiler.GetLastError().c_str());
return E_FAIL;
}
hr = device->CreateVertexShader(vs->GetBufferPointer(), vs->GetBufferSize(), nullptr, vertex.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
Name(vertex.Get(), "NeoPic.Capture.VertexShader");
hr = device->CreatePixelShader(ps->GetBufferPointer(), ps->GetBufferSize(), nullptr, pixel.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
Name(pixel.Get(), "NeoPic.Capture.PixelShader");
D3D11_SAMPLER_DESC sampler{};
sampler.AddressU = sampler.AddressV = sampler.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
sampler.MaxLOD = D3D11_FLOAT32_MAX;
sampler.ComparisonFunc = D3D11_COMPARISON_NEVER;
sampler.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;
hr = device->CreateSamplerState(&sampler, pointSampler.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
Name(pointSampler.Get(), "NeoPic.Capture.PointSampler");
sampler.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
hr = device->CreateSamplerState(&sampler, linearSampler.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
Name(linearSampler.Get(), "NeoPic.Capture.LinearSampler");
D3D11_RASTERIZER_DESC rs{};
rs.FillMode = D3D11_FILL_SOLID; rs.CullMode = D3D11_CULL_NONE; rs.DepthClipEnable = TRUE;
hr = device->CreateRasterizerState(&rs, rasterizer.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
Name(rasterizer.Get(), "NeoPic.Capture.Rasterizer");
D3D11_BLEND_DESC bs{};
auto& rt = bs.RenderTarget[0];
rt.RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
rt.SrcBlend = rt.SrcBlendAlpha = D3D11_BLEND_ONE;
rt.DestBlend = rt.DestBlendAlpha = D3D11_BLEND_ZERO;
rt.BlendOp = rt.BlendOpAlpha = D3D11_BLEND_OP_ADD;
hr = device->CreateBlendState(&bs, blend.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
Name(blend.Get(), "NeoPic.Capture.Blend");
D3D11_DEPTH_STENCIL_DESC ds{};
ds.DepthFunc = D3D11_COMPARISON_ALWAYS;
ds.StencilReadMask = D3D11_DEFAULT_STENCIL_READ_MASK;
ds.StencilWriteMask = D3D11_DEFAULT_STENCIL_WRITE_MASK;
ds.FrontFace.StencilFailOp = ds.FrontFace.StencilDepthFailOp = ds.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
ds.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
ds.BackFace = ds.FrontFace;
hr = device->CreateDepthStencilState(&ds, depth.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
Name(depth.Get(), "NeoPic.Capture.DepthStencil");
owner = device;
scratch = nextState;
return S_OK;
}

static DXGI_FORMAT SampleFormat(DXGI_FORMAT format) {
switch (format) {
case DXGI_FORMAT_B8G8R8A8_UNORM:
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
case DXGI_FORMAT_B8G8R8A8_TYPELESS: return DXGI_FORMAT_B8G8R8A8_UNORM;
case DXGI_FORMAT_R8G8B8A8_UNORM:
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
case DXGI_FORMAT_R8G8B8A8_TYPELESS: return DXGI_FORMAT_R8G8B8A8_UNORM;
case DXGI_FORMAT_B8G8R8X8_UNORM:
case DXGI_FORMAT_B8G8R8X8_UNORM_SRGB:
case DXGI_FORMAT_B8G8R8X8_TYPELESS: return DXGI_FORMAT_B8G8R8X8_UNORM;
default: return DXGI_FORMAT_UNKNOWN; // no implicit YUV/HDR conversion
}
}
public:
bool IsInitialized() const { return owner && scratch; }

// Caller drains submitted work first. Release only our cached objects;
// never modify the shared context's restored Fusion pipeline state.
void Reset() {
sampleView.Reset();
sampleTexture.Reset();
resolved.Reset();
scratch.Reset();
vertex.Reset();
pixel.Reset();
pointSampler.Reset();
linearSampler.Reset();
rasterizer.Reset();
blend.Reset();
depth.Reset();
owner.Reset();
debugNames = false;
}

HRESULT Draw(ID3D11DeviceContext* context, ID3D11Texture2D* source,
const D3D11_BOX& box, ID3D11RenderTargetView* destination,
UINT width, UINT height, bool resample) {
if (!context || !source || !destination || !width || !height) return E_INVALIDARG;
D3D11_TEXTURE2D_DESC input{};
source->GetDesc(&input);
if (box.left >= box.right || box.top >= box.bottom || box.front != 0 || box.back != 1
|| box.right > input.Width || box.bottom > input.Height || input.ArraySize != 1) return E_INVALIDARG;
const auto format = SampleFormat(input.Format);
if (format == DXGI_FORMAT_UNKNOWN) return E_NOTIMPL;
D3D11_RENDER_TARGET_VIEW_DESC targetView{};
destination->GetDesc(&targetView);
if (targetView.ViewDimension != D3D11_RTV_DIMENSION_TEXTURE2D
|| targetView.Texture2D.MipSlice != 0
|| (targetView.Format != DXGI_FORMAT_B8G8R8A8_UNORM
&& targetView.Format != DXGI_FORMAT_R8G8B8A8_UNORM)) return E_NOTIMPL;
Ptr<ID3D11Device> device, inputDevice, outputDevice;
context->GetDevice(device.GetAddressOf());
source->GetDevice(inputDevice.GetAddressOf());
destination->GetDevice(outputDevice.GetAddressOf());
if (device.Get() != inputDevice.Get() || device.Get() != outputDevice.Get()) return E_INVALIDARG;
Ptr<ID3D11Resource> resource;
destination->GetResource(resource.GetAddressOf());
Ptr<ID3D11Texture2D> output;
HRESULT hr = resource.As(&output);
if (FAILED(hr) || output.Get() == source) return E_INVALIDARG;
D3D11_TEXTURE2D_DESC target{};
output->GetDesc(&target);
if (target.Width < width || target.Height < height || target.SampleDesc.Count != 1) return E_INVALIDARG;
hr = Initialize(device.Get());
if (FAILED(hr)) return hr;
Name(output.Get(), "NeoPic.Capture.OutputTexture");
Name(destination, "NeoPic.Capture.OutputView");
Ptr<ID3D11DeviceContext1> context1;
hr = context->QueryInterface(IID_PPV_ARGS(context1.GetAddressOf()));
if (FAILED(hr)) return hr;
D3D11_TEXTURE2D_DESC sample{};
sample.Width = box.right - box.left; sample.Height = box.bottom - box.top;
sample.MipLevels = sample.ArraySize = sample.SampleDesc.Count = 1;
sample.Format = format; sample.Usage = D3D11_USAGE_DEFAULT;
sample.BindFlags = D3D11_BIND_SHADER_RESOURCE;
hr = device->CreateTexture2D(&sample, nullptr, sampleTexture.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
Name(sampleTexture.Get(), "NeoPic.Capture.SampleTexture");
ID3D11Texture2D* copySource = source;
if (input.SampleDesc.Count > 1) {
UINT support = 0;
hr = device->CheckFormatSupport(input.Format, &support);
if (FAILED(hr) || !(support & D3D11_FORMAT_SUPPORT_MULTISAMPLE_RESOLVE)) return E_NOTIMPL;
auto resolve = input;
resolve.MipLevels = 1; resolve.SampleDesc.Count = 1; resolve.SampleDesc.Quality = 0;
resolve.Usage = D3D11_USAGE_DEFAULT; resolve.BindFlags = resolve.CPUAccessFlags = resolve.MiscFlags = 0;
hr = device->CreateTexture2D(&resolve, nullptr, resolved.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
Name(resolved.Get(), "NeoPic.Capture.ResolvedTexture");
context->ResolveSubresource(resolved.Get(), 0, source, 0, input.Format);
copySource = resolved.Get();
}
context->CopySubresourceRegion(sampleTexture.Get(), 0, 0, 0, 0, copySource, 0, &box);
hr = device->CreateShaderResourceView(sampleTexture.Get(), nullptr, sampleView.ReleaseAndGetAddressOf());
if (FAILED(hr)) return hr;
Name(sampleView.Get(), "NeoPic.Capture.SampleView");
struct Restore {
ID3D11DeviceContext1* context;
Ptr<ID3DDeviceContextState> saved;
~Restore() { context->SwapDeviceContextState(saved.Get(), nullptr); }
} restore{context1.Get(), {}};
context1->SwapDeviceContextState(scratch.Get(), restore.saved.GetAddressOf());
const D3D11_VIEWPORT viewport{0, 0, static_cast<FLOAT>(width), static_cast<FLOAT>(height), 0, 1};
context->RSSetViewports(1, &viewport);
context->RSSetState(rasterizer.Get());
context->OMSetRenderTargets(1, &destination, nullptr);
context->OMSetBlendState(blend.Get(), nullptr, 0xffffffffu);
context->OMSetDepthStencilState(depth.Get(), 0);
context->IASetInputLayout(nullptr);
context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
context->VSSetShader(vertex.Get(), nullptr, 0);
context->PSSetShader(pixel.Get(), nullptr, 0);
auto sampler = resample ? linearSampler.Get() : pointSampler.Get();
context->PSSetSamplers(0, 1, &sampler);
auto view = sampleView.Get();
context->PSSetShaderResources(0, 1, &view);
const FLOAT black[4]{0, 0, 0, 1};
context->ClearRenderTargetView(destination, black);
context->Draw(3, 0);
view = nullptr;
context->PSSetShaderResources(0, 1, &view);
return S_OK;
}
};
Loading
Loading