// render_core — M2 gate 4 verification app. // // Per-eye offscreen render of the world-fixed test quad at panel-half // resolution, viewed with AHRS pose + ±ipd/2, projected through the // closed-form GetProjectionRaw fold. Desktop debug window shows the stereo // pair side by side (downscaled). Gate: correct stereo pair geometry; quad // counter-rotates against headset motion. // // Usage: // render_core --config (AHRS pose, live window) // render_core --config c.json --pose 10,0,0 --dump out (headless: fixed // yaw/pitch/roll deg, render once, write out_left.bmp/out_right.bmp) // // Keys (window focused): R = recenter, B = AHRS bias recapture, ESC = quit. // // Conventions: world +Y up, -Z forward. Eye RT uv: u right, v DOWN (v=0 is // top — S3). The projection's T/B tangents are y-down; the proj matrix // flips so clip-space y-up renders into the y-down RT correctly when the // image is later sampled by the warp with v=0=top. #include "calib/hmd_config.h" #include "device/tundra_imu.h" #include "render/scene_renderer.h" #include "track/ahrs.h" #include #include #include #include #include #define _USE_MATH_DEFINES #include #include #include #include #include #include using Microsoft::WRL::ComPtr; using namespace sauna; namespace { // ---- matrices: row-major storage, column-vector math (clip = M v) ---- void matMul(const float a[16], const float b[16], float out[16]) { float r[16]; for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) { r[i * 4 + j] = 0; for (int k = 0; k < 4; k++) r[i * 4 + j] += a[i * 4 + k] * b[k * 4 + j]; } memcpy(out, r, sizeof(r)); } // World<-head quaternion -> view matrix for an eye at head-frame offset t_e: // v_eye = R^T v_world - t_e. void viewFromPose(const Quat& q, const float tEye[3], float out[16]) { const double w = q.w, x = q.x, y = q.y, z = q.z; // R (world<-head), row-major. const double R[9] = {1 - 2 * (y * y + z * z), 2 * (x * y - w * z), 2 * (x * z + w * y), 2 * (x * y + w * z), 1 - 2 * (x * x + z * z), 2 * (y * z - w * x), 2 * (x * z - w * y), 2 * (y * z + w * x), 1 - 2 * (x * x + y * y)}; memset(out, 0, 16 * sizeof(float)); for (int r = 0; r < 3; r++) for (int c = 0; c < 3; c++) out[r * 4 + c] = (float)R[c * 3 + r]; // R^T out[3] = -tEye[0]; out[7] = -tEye[1]; out[11] = -tEye[2]; out[15] = 1.0f; } // Asymmetric projection from raw tangents (L,R,T,B per ProjectionRawTangents: // y-down, T = top). Looking down -Z, D3D clip z in [0,1]. void projFromTangents(double L, double R, double T, double B, float zn, float zf, float out[16]) { const double u = -T, d = -B; // y-up tangents: up, down memset(out, 0, 16 * sizeof(float)); out[0] = (float)(2.0 / (R - L)); out[2] = (float)((R + L) / (R - L)); out[5] = (float)(2.0 / (u - d)); out[6] = (float)((u + d) / (u - d)); out[10] = zf / (zn - zf); out[11] = zn * zf / (zn - zf); out[14] = -1.0f; } Quat quatFromEulerDeg(double yawDeg, double pitchDeg, double rollDeg) { const double k = 3.14159265358979323846 / 180.0 / 2.0; const double cy = cos(yawDeg * k), sy = sin(yawDeg * k); const double cp = cos(pitchDeg * k), sp = sin(pitchDeg * k); const double cr = cos(rollDeg * k), sr = sin(rollDeg * k); // q = Ry(yaw) * Rx(pitch) * Rz(roll) (matches ahrs_log's toEuler) Quat qy{cy, 0, sy, 0}, qx{cp, sp, 0, 0}, qz{cr, 0, 0, sr}; auto mul = [](const Quat& a, const Quat& b) { return Quat{a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z, a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y, a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x, a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w}; }; return mul(mul(qy, qx), qz); } // ---- blit shader (fullscreen triangle sampling an eye texture) ---- const char kBlit[] = R"( Texture2D tex : register(t0); SamplerState smp : register(s0); struct VSOut { float4 pos : SV_Position; float2 uv : TEXCOORD0; }; VSOut vsmain(uint id : SV_VertexID) { VSOut o; float2 uv = float2((id << 1) & 2, id & 2); o.pos = float4(uv * float2(2, -2) + float2(-1, 1), 0, 1); o.uv = uv; return o; } float4 psmain(VSOut i) : SV_Target { return tex.Sample(smp, i.uv); } )"; bool compileBlit(const char* entry, const char* target, ComPtr* out) { ComPtr err; if (FAILED(D3DCompile(kBlit, sizeof(kBlit) - 1, nullptr, nullptr, nullptr, entry, target, 0, 0, &*out, &err))) { fprintf(stderr, "blit %s: %s\n", entry, err ? (const char*)err->GetBufferPointer() : "failed"); return false; } return true; } bool writeBmp(const char* path, const uint8_t* rgba, uint32_t w, uint32_t h, uint32_t pitch) { FILE* f = fopen(path, "wb"); if (!f) return false; const uint32_t imgSize = w * h * 4; #pragma pack(push, 1) struct { uint16_t magic = 0x4D42; uint32_t size; uint32_t rsvd = 0; uint32_t offset = 54; uint32_t hdrSize = 40; int32_t w, h; uint16_t planes = 1, bpp = 32; uint32_t comp = 0, imgSize; int32_t ppmX = 2835, ppmY = 2835; uint32_t clrUsed = 0, clrImp = 0; } hdr; #pragma pack(pop) hdr.size = 54 + imgSize; hdr.w = (int32_t)w; hdr.h = -(int32_t)h; // top-down hdr.imgSize = imgSize; fwrite(&hdr, sizeof(hdr), 1, f); std::vector row(w * 4); for (uint32_t y = 0; y < h; y++) { const uint8_t* s = rgba + y * pitch; for (uint32_t x = 0; x < w; x++) { // RGBA -> BGRA row[x * 4 + 0] = s[x * 4 + 2]; row[x * 4 + 1] = s[x * 4 + 1]; row[x * 4 + 2] = s[x * 4 + 0]; row[x * 4 + 3] = s[x * 4 + 3]; } fwrite(row.data(), 1, row.size(), f); } fclose(f); return true; } bool g_quit = false; bool g_recenter = false; bool g_bias = false; LRESULT CALLBACK wndProc(HWND h, UINT m, WPARAM wp, LPARAM lp) { switch (m) { case WM_DESTROY: g_quit = true; PostQuitMessage(0); return 0; case WM_KEYDOWN: if (wp == VK_ESCAPE) g_quit = true; if (wp == 'R') g_recenter = true; if (wp == 'B') g_bias = true; return 0; } return DefWindowProcW(h, m, wp, lp); } } // namespace int main(int argc, char** argv) { setbuf(stdout, nullptr); std::string configPath, dumpPrefix; const char* imuSerial = ""; bool havePose = false; double poseYpr[3] = {0, 0, 0}; for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "--config") && i + 1 < argc) configPath = argv[++i]; else if (!strcmp(argv[i], "--dump") && i + 1 < argc) dumpPrefix = argv[++i]; else if (!strcmp(argv[i], "--imu-serial") && i + 1 < argc) imuSerial = argv[++i]; else if (!strcmp(argv[i], "--pose") && i + 1 < argc) { havePose = true; sscanf(argv[++i], "%lf,%lf,%lf", &poseYpr[0], &poseYpr[1], &poseYpr[2]); } else { fprintf(stderr, "usage: render_core --config [--pose y,p,r] " "[--dump prefix] [--imu-serial LHR-...]\n"); return 2; } } if (configPath.empty()) { fprintf(stderr, "--config is required\n"); return 2; } HmdConfig cfg; std::string err; if (!LoadHmdConfig(configPath, &cfg, &err)) { fprintf(stderr, "%s\n", err.c_str()); return 1; } const uint32_t eyeW = cfg.eye_width_px, eyeH = cfg.eye_height_px; double frus[2][4]; for (int e = 0; e < 2; e++) ProjectionRawTangents(cfg.eye[e], &frus[e][0], &frus[e][1], &frus[e][2], &frus[e][3]); printf("unit %s eye %ux%u ipd %.1f mm\n", cfg.serial.c_str(), eyeW, eyeH, cfg.ipd_default_mm); for (int e = 0; e < 2; e++) printf("%s frustum L%+.5f R%+.5f T%+.5f B%+.5f\n", e ? "right" : "left ", frus[e][0], frus[e][1], frus[e][2], frus[e][3]); // Pose source. TundraImu imu; Ahrs ahrs; bool useImu = !havePose; if (useImu) { Ahrs::Params ap; ahrs.configure(cfg, ap); imu.setSampleSink([&ahrs](const ImuSample& s) { ahrs.update(s); }); if (!imu.start(imuSerial)) { fprintf(stderr, "no IMU — pass --pose y,p,r for a fixed pose\n"); return 2; } printf("AHRS pose from %s (capture bias: hold still ~1 s)\n", imu.connectedSerial().c_str()); if (!cfg.serial.empty() && imu.connectedSerial() != cfg.serial) printf("WARNING: config %s != attached unit %s\n", cfg.serial.c_str(), imu.connectedSerial().c_str()); } // ---- D3D12 device ---- ComPtr dev; if (FAILED(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&dev)))) { fprintf(stderr, "D3D12CreateDevice failed\n"); return 1; } ComPtr queue; D3D12_COMMAND_QUEUE_DESC qd{D3D12_COMMAND_LIST_TYPE_DIRECT}; dev->CreateCommandQueue(&qd, IID_PPV_ARGS(&queue)); ComPtr alloc; dev->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&alloc)); ComPtr list; dev->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, alloc.Get(), nullptr, IID_PPV_ARGS(&list)); list->Close(); ComPtr fence; dev->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)); HANDLE fenceEv = CreateEventW(nullptr, FALSE, FALSE, nullptr); uint64_t fenceVal = 0; auto sync = [&] { queue->Signal(fence.Get(), ++fenceVal); fence->SetEventOnCompletion(fenceVal, fenceEv); WaitForSingleObject(fenceEv, 5000); }; // ---- eye render targets ---- const DXGI_FORMAT kFmt = DXGI_FORMAT_R8G8B8A8_UNORM; ComPtr eyeTex[2]; ComPtr rtvHeap; D3D12_DESCRIPTOR_HEAP_DESC rh{D3D12_DESCRIPTOR_HEAP_TYPE_RTV, 4}; dev->CreateDescriptorHeap(&rh, IID_PPV_ARGS(&rtvHeap)); const UINT rtvStep = dev->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); D3D12_CPU_DESCRIPTOR_HANDLE eyeRtv[2]; { D3D12_RESOURCE_DESC td{}; td.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; td.Width = eyeW; td.Height = eyeH; td.DepthOrArraySize = 1; td.MipLevels = 1; td.Format = kFmt; td.SampleDesc.Count = 1; td.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; D3D12_HEAP_PROPERTIES def{D3D12_HEAP_TYPE_DEFAULT}; D3D12_CLEAR_VALUE cv{kFmt, {0.05f, 0.05f, 0.08f, 1.0f}}; for (int e = 0; e < 2; e++) { if (FAILED(dev->CreateCommittedResource( &def, D3D12_HEAP_FLAG_NONE, &td, D3D12_RESOURCE_STATE_RENDER_TARGET, &cv, IID_PPV_ARGS(&eyeTex[e])))) { fprintf(stderr, "eye RT alloc failed\n"); return 1; } eyeRtv[e] = rtvHeap->GetCPUDescriptorHandleForHeapStart(); eyeRtv[e].ptr += e * rtvStep; dev->CreateRenderTargetView(eyeTex[e].Get(), nullptr, eyeRtv[e]); } } SceneRenderer scene; if (!scene.init(dev.Get(), kFmt)) return 1; // Eye offsets: left at -ipd/2 (OpenVR GetEyeToHeadTransform convention). const float ipdM = (float)(cfg.ipd_default_mm * 0.001); const float tEye[2][3] = {{-ipdM / 2, 0, 0}, {+ipdM / 2, 0, 0}}; auto buildViewProj = [&](const Quat& q, int e, float out[16]) { float V[16], P[16]; viewFromPose(q, tEye[e], V); projFromTangents(frus[e][0], frus[e][1], frus[e][2], frus[e][3], 0.05f, 100.0f, P); matMul(P, V, out); }; auto renderEyes = [&](const Quat& q) { alloc->Reset(); list->Reset(alloc.Get(), nullptr); for (int e = 0; e < 2; e++) { float vp[16]; buildViewProj(q, e, vp); scene.record(list.Get(), eyeRtv[e], eyeW, eyeH, vp); } }; // ---- dump mode: render once, read back, write BMPs, exit ---- if (!dumpPrefix.empty()) { Quat q = quatFromEulerDeg(poseYpr[0], poseYpr[1], poseYpr[2]); if (useImu) { printf("waiting for AHRS init...\n"); for (int i = 0; i < 100 && !ahrs.status().initialized; i++) std::this_thread::sleep_for(std::chrono::milliseconds(100)); q = ahrs.pose(); } renderEyes(q); // Readback. D3D12_RESOURCE_DESC td = eyeTex[0]->GetDesc(); D3D12_PLACED_SUBRESOURCE_FOOTPRINT fp{}; UINT64 total = 0; dev->GetCopyableFootprints(&td, 0, 1, 0, &fp, nullptr, nullptr, &total); ComPtr rb[2]; D3D12_HEAP_PROPERTIES rbHeap{D3D12_HEAP_TYPE_READBACK}; D3D12_RESOURCE_DESC bd{}; bd.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; bd.Width = total; bd.Height = 1; bd.DepthOrArraySize = 1; bd.MipLevels = 1; bd.SampleDesc.Count = 1; bd.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; for (int e = 0; e < 2; e++) { dev->CreateCommittedResource(&rbHeap, D3D12_HEAP_FLAG_NONE, &bd, D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(&rb[e])); D3D12_RESOURCE_BARRIER bar{}; bar.Transition.pResource = eyeTex[e].Get(); bar.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; bar.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE; bar.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; list->ResourceBarrier(1, &bar); D3D12_TEXTURE_COPY_LOCATION src{eyeTex[e].Get(), D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX}; D3D12_TEXTURE_COPY_LOCATION dst{rb[e].Get(), D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT}; dst.PlacedFootprint = fp; list->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr); } list->Close(); ID3D12CommandList* lists[] = {list.Get()}; queue->ExecuteCommandLists(1, lists); sync(); for (int e = 0; e < 2; e++) { uint8_t* p = nullptr; rb[e]->Map(0, nullptr, (void**)&p); std::string path = dumpPrefix + (e ? "_right.bmp" : "_left.bmp"); writeBmp(path.c_str(), p, eyeW, eyeH, fp.Footprint.RowPitch); rb[e]->Unmap(0, nullptr); printf("wrote %s\n", path.c_str()); } if (useImu) imu.stop(); return 0; } // ---- live window ---- WNDCLASSW wc{}; wc.lpfnWndProc = wndProc; wc.hInstance = GetModuleHandleW(nullptr); wc.lpszClassName = L"sauna_render_core"; wc.hCursor = LoadCursorW(nullptr, (LPCWSTR)IDC_ARROW); RegisterClassW(&wc); const uint32_t winW = 1272, winH = 636; RECT wr{0, 0, (LONG)winW, (LONG)winH}; AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE); HWND hwnd = CreateWindowW(wc.lpszClassName, L"sauna render_core — R recenter, B bias, ESC quit", WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, wr.right - wr.left, wr.bottom - wr.top, nullptr, nullptr, wc.hInstance, nullptr); ComPtr factory; CreateDXGIFactory1(IID_PPV_ARGS(&factory)); ComPtr swap; { DXGI_SWAP_CHAIN_DESC1 sd{}; sd.Width = winW; sd.Height = winH; sd.Format = kFmt; sd.SampleDesc.Count = 1; sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.BufferCount = 2; sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; ComPtr sc1; if (FAILED(factory->CreateSwapChainForHwnd(queue.Get(), hwnd, &sd, nullptr, nullptr, &sc1))) { fprintf(stderr, "swapchain failed\n"); return 1; } sc1.As(&swap); } ComPtr back[2]; D3D12_CPU_DESCRIPTOR_HANDLE backRtv[2]; for (int i = 0; i < 2; i++) { swap->GetBuffer(i, IID_PPV_ARGS(&back[i])); backRtv[i] = rtvHeap->GetCPUDescriptorHandleForHeapStart(); backRtv[i].ptr += (2 + i) * rtvStep; dev->CreateRenderTargetView(back[i].Get(), nullptr, backRtv[i]); } // Blit pipeline (eye texture -> window half). ComPtr blitRs; ComPtr blitPso; ComPtr srvHeap; { D3D12_DESCRIPTOR_RANGE range{}; range.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV; range.NumDescriptors = 1; D3D12_ROOT_PARAMETER param{}; param.ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; param.DescriptorTable.NumDescriptorRanges = 1; param.DescriptorTable.pDescriptorRanges = ⦥ param.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; D3D12_STATIC_SAMPLER_DESC samp{}; samp.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; samp.AddressU = samp.AddressV = samp.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; samp.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; D3D12_ROOT_SIGNATURE_DESC rs{}; rs.NumParameters = 1; rs.pParameters = ¶m; rs.NumStaticSamplers = 1; rs.pStaticSamplers = &samp; ComPtr sig, serr; D3D12SerializeRootSignature(&rs, D3D_ROOT_SIGNATURE_VERSION_1, &sig, &serr); dev->CreateRootSignature(0, sig->GetBufferPointer(), sig->GetBufferSize(), IID_PPV_ARGS(&blitRs)); ComPtr vs, ps; if (!compileBlit("vsmain", "vs_5_0", &vs) || !compileBlit("psmain", "ps_5_0", &ps)) return 1; D3D12_GRAPHICS_PIPELINE_STATE_DESC pd{}; pd.pRootSignature = blitRs.Get(); pd.VS = {vs->GetBufferPointer(), vs->GetBufferSize()}; pd.PS = {ps->GetBufferPointer(), ps->GetBufferSize()}; pd.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; pd.SampleMask = UINT_MAX; pd.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; pd.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; pd.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; pd.NumRenderTargets = 1; pd.RTVFormats[0] = kFmt; pd.SampleDesc.Count = 1; dev->CreateGraphicsPipelineState(&pd, IID_PPV_ARGS(&blitPso)); D3D12_DESCRIPTOR_HEAP_DESC hd{}; hd.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; hd.NumDescriptors = 2; hd.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; dev->CreateDescriptorHeap(&hd, IID_PPV_ARGS(&srvHeap)); const UINT srvStep = dev->GetDescriptorHandleIncrementSize( D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); for (int e = 0; e < 2; e++) { D3D12_SHADER_RESOURCE_VIEW_DESC sv{}; sv.Format = kFmt; sv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; sv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; sv.Texture2D.MipLevels = 1; D3D12_CPU_DESCRIPTOR_HANDLE h = srvHeap->GetCPUDescriptorHandleForHeapStart(); h.ptr += e * srvStep; dev->CreateShaderResourceView(eyeTex[e].Get(), &sv, h); } } const UINT srvStep = dev->GetDescriptorHandleIncrementSize( D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); printf("live window up — rotate the headset; the quad must counter-rotate " "(stay world-fixed)\n"); uint64_t frames = 0; auto t0 = std::chrono::steady_clock::now(); Quat fixedQ = quatFromEulerDeg(poseYpr[0], poseYpr[1], poseYpr[2]); while (!g_quit) { MSG msg; while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageW(&msg); } if (g_quit) break; if (g_recenter) { g_recenter = false; ahrs.recenter(); printf("[recentered]\n"); } if (g_bias) { g_bias = false; ahrs.requestBiasCapture(); printf("[bias recapture — hold still]\n"); } Quat q = fixedQ; if (useImu) { auto st = ahrs.status(); if (st.initialized) q = ahrs.pose(); } renderEyes(q); // Eyes -> SRV, draw both halves of the backbuffer, present. const UINT bi = swap->GetCurrentBackBufferIndex(); D3D12_RESOURCE_BARRIER bars[3]{}; for (int e = 0; e < 2; e++) { bars[e].Transition.pResource = eyeTex[e].Get(); bars[e].Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; bars[e].Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; bars[e].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; } bars[2].Transition.pResource = back[bi].Get(); bars[2].Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT; bars[2].Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; bars[2].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; list->ResourceBarrier(3, bars); list->OMSetRenderTargets(1, &backRtv[bi], FALSE, nullptr); list->SetGraphicsRootSignature(blitRs.Get()); list->SetPipelineState(blitPso.Get()); ID3D12DescriptorHeap* heaps[] = {srvHeap.Get()}; list->SetDescriptorHeaps(1, heaps); list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); D3D12_RECT sc{0, 0, (LONG)winW, (LONG)winH}; list->RSSetScissorRects(1, &sc); for (int e = 0; e < 2; e++) { D3D12_VIEWPORT vp{e * (winW / 2.0f), 0, winW / 2.0f, (float)winH, 0, 1}; list->RSSetViewports(1, &vp); D3D12_GPU_DESCRIPTOR_HANDLE h = srvHeap->GetGPUDescriptorHandleForHeapStart(); h.ptr += e * srvStep; list->SetGraphicsRootDescriptorTable(0, h); list->DrawInstanced(3, 1, 0, 0); } for (int e = 0; e < 2; e++) { bars[e].Transition.StateBefore = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; bars[e].Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; } bars[2].Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; bars[2].Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT; list->ResourceBarrier(3, bars); list->Close(); ID3D12CommandList* lists[] = {list.Get()}; queue->ExecuteCommandLists(1, lists); swap->Present(1, 0); sync(); frames++; if (frames % 120 == 0 && useImu) { auto st = ahrs.status(); double secs = std::chrono::duration( std::chrono::steady_clock::now() - t0).count(); printf("fps %.0f ahrs %s |a| %.3f g samples %llu resets %llu\n", frames / secs, st.initialized ? "ok" : "bias-capture", st.accelMagG, (unsigned long long)st.samples, (unsigned long long)st.resets); } } sync(); if (useImu) imu.stop(); printf("frames: %llu\n", (unsigned long long)frames); return 0; }