Single-pass stereo: render both eyes in one geometry walk - #45
Merged
Conversation
The tools/ scripts generate __pycache__/*.pyc on import.
The plan for single-pass stereo -- instance-doubled, double-wide, viewport-routed, G-buffer-range only -- with the exact vertex-shader transform (remap the per-eye cb0 view rows to a relative-indexed cb13 selected by SV_InstanceID & 1, emit SV_ViewportArrayIndex) and the phased build. Records the operand census the transform is sized against, established by walking the SM5 token stream of the whole shader corpus: 0 parse failures on 455 vertex shaders, and a 155-shader model family that reads the global view-projection.
A pure, portable crate for the vertex-shader bytecode surgery that makes one instanced draw render both eyes. This lands the analysis foundation: a DXBC container parser and an SM5 token-stream walker that locate the per-eye cb0 operands (camera position cb0[4], view-projection cb0[29..32]) the rewrite must retarget to a per-eye cb13. Kept free of platform deps so it unit-tests natively against the game's extracted vertex shaders (which stay git-ignored): the tests reproduce the sh_0067 fingerprint (17 refs, in order) and sweep the whole corpus (455 VS, 0 parse failures, the 155-shader model family), and skip cleanly where the shaders are absent.
… shader Compiling a reference VS through a D3DCompile-under-wine harness settles the encodings the transform must produce, recorded in the design doc: vs_5_0 accepts SV_ViewportArrayIndex but adds an SFI0 chunk (bit 13, viewport/RT-array-index-from-any-shader), the ISGN/OSGN sysvalue encodings for SV_InstanceID / SV_ViewportArrayIndex, and cb13 as dynamicIndexed.
patch_vertex_shader rewrites a model vertex shader for instance-doubled, viewport-routed single-pass stereo: binds both eyes' five cb0 position rows at cb13, remaps every per-eye cb0 operand to cb13[rBase.x + k], injects the SV_InstanceID input, the SV_ViewportArrayIndex output, and the SFI0 feature bit, then fixes the SHEX length and the container checksum. Hoists the DXBC checksum out of the payload into the crate so the rewrite and the fragment-program patching in hooks/graphics_engine/ shader.rs share one implementation.
Replace disasm.c (and its clang+xwin .sh builder) with a dxbc-tool workspace crate that calls D3DCompile/D3DDisassemble through the windows crate, driven by one scripts/dxbc.sh wrapper. No more manual LoadLibrary/GetProcAddress, and Rust's stdout writes the DXBC blob verbatim -- the text-mode CR-LF corruption a C harness writing binary to stdout has to guard against cannot occur.
Groundwork for single-pass stereo, all observation-only so it is safe to inject with rendering unchanged: - config: stereo.single_pass (master, off) + single_pass_patch_dryrun. - stereo::single_pass: the DXVK VPAndRTArrayIndexFromAnyShaderFeeding- Rasterizer capability probe, and a census of the vertex-shader rewrite (patched / no-per-eye-refs / errored). - a CreateVertexProgram detour that runs patch_vertex_shader on every VS and tallies the outcome without substituting the bytecode. - a Render-tab section reporting the probe result and the live census. The census validates the DXBC rewriter against the game's real shader set and reports the true Phase-1 scope before the rest of the pipeline lands.
Run patch_vertex_shader over every vertex shader in the extracted bundle and structurally validate each success (re-parses, SFI0 viewport bit set, no residual per-eye cb0 operand, clean token walk). The test drove out three real bugs the single-shader tests missed: - immediate_component_count mapped the 1-component enum to 4 dwords, so the parser over-read every scalar l(x) immediate by 3 tokens and drifted -- silently corrupting the ~17 shaders that use them. - parse_operand could return a next index past the token slice, which the operand iterator tolerated but the rewriter sliced with, panicking. Made parse_operand total via end_operand's bounds check. - per_eye_refs counted a dcl_constantbuffer operand (whose element is the buffer size, e.g. cb0[29]) as a per-eye reference. Skip declarations, via a shared Instruction::is_declaration. Corpus result: 455 VS -> 196 patched, 245 no-per-eye-refs, 14 deferred (already declare SV_InstanceID), 0 errored, 0 structurally invalid.
…hecklist Capture the in-game architecture synthesised from the engine RE: the hook points with addresses, the six-stage pipeline (what's built vs unbuilt), the corpus-validated shader breakdown (196 patched / 245 no-refs / 14 deferred / 0 errored), a ranked wake-up test checklist, and the deliberately-deferred later-phase work.
All 196 patched blobs pass real Microsoft D3DDisassemble under wine, and a patched game shader disassembles to the exact expected stereo idiom.
Live in-game census confirmed the offline corpus exactly (392/490/28 = 2× 196/245/14, the ×2 from the reload's bundle bounce). Refine the UI it surfaced: classify the SV_InstanceID deferrals into their own bucket so "errored" means genuinely-unexpected (and should read 0), and reset the census after the throwaway away-bundle pass so the count is a clean 1×.
The first testable slice of the in-game pipeline, expected to render identically to the double-draw (no double-wide target, instancing, or viewport routing yet): - When single-pass is active (master on, dry-run off, capability present), the CreateVertexProgram hook substitutes the patched bytecode, so the ~196 model-family VS read their position from cb13 instead of cb0. - A SetAllGlobalShaderProgramConstants detour mirrors the current view's per-eye cb0 rows (m_VPGlobalConstData[29..32] + [4]) into a mod-owned cb13 and binds it at b13, per pass. Both eye slots get the same current-view rows, so a patched shader renders exactly what it would from cb0 -- in the G-buffer and in the shadow/reflection passes that reuse the same model shaders (the shadow-safety reason cb13 tracks the live view rather than being written once). Two details the slice does not work without. The substitution repoints CreateVertexProgramParams.m_Size as well as m_Code: the rewrite grows the DXBC (added SFI0 chunk, cb13 declaration, prologue, signature entries), and leaving the original length hands the D3D stack a truncated container whose chunk table runs past its declared size, which DXVK aborts reading out of bounds. And viewport slot 1 is mirrored by a detour on ID3D11DeviceContext::RSSetViewports (with RSSetScissorRects) rather than at any engine-level seam, because the shadow cascades set their per-cascade atlas sub-rect with a raw RSSetViewports between render-setup binds that no engine call observes -- so a single-viewport set has to become two identical slots wherever it originates, or odd-instance shadow casters route to a stale region and the shadows flicker. The detours install lazily on the first active frame, under a thread suspender, and only when single-pass is switched on, so a normal session never patches the vtable. Still gated behind stereo.single_pass (off). Milestone A validates the patch + cb13 path end to end before the doubling/routing lands.
…, instancing The render-thread half of Milestone B, gated behind stereo.single_pass_dual_eye (itself under the master), applied only in the G-buffer geometry pass range (RP_Z_OCCLUDERS..RP_FIRST_SCENE, tracked around DrawRenderPassRange): - cb13 filled with DISTINCT per-eye view-projections: replicate the double-draw per-eye camera math (offset the pristine center transform by each eye's world_offset + orientation_delta, invert, zero-translation for the OffsetVP, reverse-Z eye projection) purely in mod code, so one walk produces both eyes. Falls back to the Milestone A mirror outside the scene range. - RSSetViewports detour splits the bound viewport into left/right halves for SV_ViewportArrayIndex routing (instead of two identical copies). - a DrawIndexed COM-vtable detour promotes non-instanced draws to 2 instances, so SV_InstanceID & 1 selects the eye. Already-instanced draws are left alone. Still needs the double-wide target + the single-walk collapse (next) for a finished image; on its own each eye renders into half a per-eye target. All off by default.
…up order Record that Milestone A is confirmed in-game, the Milestone B render machinery is built (dual-eye cb13, eye-half viewports, instance doubling) and testable as a diagnostic, and the two coupled remaining pieces (double-wide RT + capture split, single-walk collapse) with the exact approach and the wake-up bring-up order.
The linchpin for handling unpatched geometry. Two more COM-vtable detours (installed with the others, only when single-pass is active): the device's CreateVertexShader (slot 12) records the ID3D11VertexShader created from a patched blob into a set (the CreateVertexProgram hook flags the pending patched creation via PATCH_PENDING), and the context's VSSetShader (slot 11) caches whether the now-bound shader is in that set. DrawIndexed then gates on it: patched shaders are instance-doubled (both eyes in one draw), unpatched are left single (their per-eye double-draw is the next step). Logs the per-frame patched/unpatched G-buffer draw split so the gating can be verified before building on it. Patched-VS set clears on shader reload (pointers are released and could be reused).
Single-pass leaves the game holding its edits: substituted (cb13-reading) vertex shaders, the fragment PCF/dissolve patches, and inline COM-vtable detours on the DXVK functions. Without teardown the render thread also keeps running through the whole eject, racing the hook uninstall and D3D release -- the crash-on-uninject. Gate the single-pass render path on `is_shutting_down()` so it goes inert the instant F5 is pressed; disable the COM detours (under a thread suspender, like install) before the payload unloads so no dangling jump survives into freed code; and on eject bounce the shader bundle to re-create the pristine originals, with both the vertex substitution and the fragment patches skipped while shutting down.
The main G-buffer render setup binds its viewport before DrawRenderPassRange raises the in-range flag, so rs_set_viewports_detour identical-dups it instead of splitting into eye halves -- and that dup covers the bulk of the geometry. Re-split the bound viewport the moment the range flag goes up (dual-eye only), and add per-frame split/identical-dup counters to the draw-split log for bring-up visibility.
Catching a hard crash under Wine needs two traps handled: Wine fires SIGUSR1 and realtime signals constantly for its own scheduling, and gdb asserts if it reports a signal while the game's threads are exiting mid-crash. catch_crash.py auto-resolves the game PID and exe/payload address bands, silences the Wine signals, stops on the first genuine SIGSEGV/SIGABRT, and dumps registers, faulting instructions, a backtrace, and a stack scan -- tagging any address in the exe or payload band, the signature of a dangling detour after teardown. Documented in the skill alongside the wine-signal and thread-exit traps.
One `game.Draw` now produces both eyes when `single_pass_collapse` is on (requires `single_pass_dual_eye`): `game_update_render` runs a single dispatch (`&[(0, false)]`), dropping the per-eye loop and the between-eye snapshot/restore; `setup_render_camera` keeps the render camera centered (both eyes come from `cb13`, shadow-anchor delta zeroed); and `render_engine_post_draw` splits the one back buffer into the two eye textures. Eye routing spans the whole camera scene, not just the G-buffer: with no second dispatch the later geometry passes (water, sky, transparents) must route to the eyes too, while the interleaved fullscreen deferred-lighting/post passes must keep the full width. `render_pass.rs` marks the whole scene (`first >= RP_Z_OCCLUDERS`, excluding the shadow/reflection prepasses), and the viewport split moves from pass-level to draw-level: `draw_indexed_detour` splits into the eye halves for a patched geometry draw, while a new `Draw` (slot 13) detour resets the fullscreen passes to full width. Two properties of the eye split that are load-bearing, both found by getting them wrong first: - `ensure_collapse_viewport` re-binds the split unconditionally before every routed draw rather than caching whether it is already split. The engine can change the viewport underneath it (a `count != 1` set, or a path the detour does not observe), and a stale cached flag lands both instances of a patched draw in one eye-half -- geometry appearing twice in one eye and missing from the other. Re-binding is cheap at a few hundred geometry draws per frame, far below the draw budget the collapse is cutting. - The flip is exempt from eye-parity gating. Collapsed, the dispatch list is a single entry tagged eye 0 while `present_eye_0` defaults to false and so selects eye 1, making `present_eye != eye` true on the only dispatch there is, every frame: the engine runs on at full rate, submitting draws and logging stats, while nothing reaches the screen. Because `graphics_flip` also renders the egui overlay, that presents as a total lockup. Parity is meaningless here -- the one dispatch carries both eyes. Squished until `single_pass_double_wide` (each half is half a per-eye target); particle `DrawInstanced` / already-instanced draws are not yet routed.
`single_pass_double_wide` (requires `single_pass_collapse` + native resolution) re-creates the scene render targets at 2x per-eye width, so the collapsed single walk renders both eye-halves side by side at full resolution instead of squished. `vr::engine_render_resolution` returns 2x the per-eye width, which the existing deferred-`ApplyResize` native-resolution driver targets -- so the whole scene RT set (back buffer, `m_BackBufferLinear`, the G-buffers) goes double-wide and the per-pass viewport follows automatically. The XR swapchain stays per-eye width (`native_eye_resolution` is unchanged), and the per-eye capture textures are sized to half the back buffer, so the collapse's capture split copies each full-width half straight into its eye texture. Unpatched geometry (which reads `cb0`, not the per-eye `cb13`) now renders at the 2x-wide aspect, and screen-space/post passes run once over the double-wide target -- both known bring-up limitations.
The native-resolution shutdown restore was deferred (write the pending size, let the engine's `HandleModeChange` service it in a later `Draw` prologue) -- but eject renders no further frames, so it never completed and the game was left stuck at the mod's render size. With double-wide that is a glaring 2x-wide stretch; it affected every native-resolution eject. `on_shutdown` now calls `ApplyResize` directly on the (already-drained) idle context and clears the engine's deferred mode-change flag, so the original size is restored before the hooks are torn down.
F12 writes a PNG of the linear back buffer to a DLL-adjacent `screenshots/` folder (under collapse, the full side-by-side render -- both eyes) plus a `.json` sidecar dumping the frame's single-pass matrix state: per-eye `world_offset`, `orientation_delta`, `projection_reverse_z`, the uploaded `cb13` view-projection and camera position, the centre transform, the view divergence, and the config. A robust alternative to the F10 fullscreen capture for in-headset diagnosis, and it turns matrix debugging into reading numbers instead of squinting at captures. Also: a rate-limited `cb13 eye divergence` log line, and a one-click "WIP setup" button in the Render tab that enables the full single-pass config (single-pass + dual-eye + collapse + double-wide + native resolution) and reloads the shaders.
In the collapsed single walk the render camera is centered and the target is double-wide, so the mod's overlays were drawn once and stretched: the floating HUD/egui panels projected at the centre across both eye-halves, and the flat egui debug overlay laid out at double width (half-relative-size). `render_engine_post_draw` now draws the HUD panel and egui panel once per eye, each into its eye-half viewport (bound via the un-detoured `RSSetViewports` so the collapse detour does not dup it) with that eye's own full view-projection (`single_pass::collapse_ui_eye_override` / `full_eye_view_projection`). The flat egui overlay lays out and renders at per-eye width, and the mirror composite stretches it across the double-wide buffer, which the window's 2x squish cancels. All collapse-gated, so the double-draw VR path is unchanged.
Records the RE of the non-cb0 geometry families and how to single-pass them: which are already shader-patched and working (buildings, depth, occluders), which are patched but not yet routed (skinned characters/NPCs -- a draw-path fix, not a shader one), and which need clip-space output reprojection (baked-WVP, tessellated terrain, GPU-indirect vegetation), with the DXBC recipe and order.
Bump the pyxis dependency and the pyxis-defs submodule for pyxis's improved constant support, and regenerate the bindings. Generated string constants are now `&'static CStr` rather than `&'static str`, matching how the game consumes them. Downstream: the two sites that rebuilt a `CString` from those constants now pass the `&CStr` straight through -- `WeatherController::EVENT_*` in the weather UI and `EventIdSymbolTable::ACT_*` in `resolve_act_id`.
… directory Every disk artifact -- the log, crash log, profiler captures, render traces, grapple telemetry, and screenshots -- now lands under one timestamped folder beside the DLL, sessions/<stamp>/, instead of scattering timestamped files across the DLL directory. A new session module resolves the root once at startup (before logging installs) and hands out the root, for the single-file log and crash log, and per-kind subdirectories, created lazily so a run only materializes the folders it uses. Timestamp generation is centralized in session::stamp, replacing the three ad-hoc jiff strftime sites. Screenshots gain their own <stamp>/ subfolder, mirroring the render-trace layout. The crash-log docs are corrected: under per-session folders jc3vrs.log is no longer truncated across runs, so the crash log's reason to exist is that it is a handler-safe, allocation-free raw-handle sink, not that it survives truncation.
The capture window's fullscreen toggle is finicky under some setups; gate it behind a const F10_CAPTURE_ENABLED. It is a runtime `if`, so VK_F10 stays compiled and it is a one-line flip to restore. The F12 screenshot path covers in-headset diagnosis meanwhile.
Replace the one-shot "WIP setup" button with a single toggle at the top of the Single-pass section that flips the whole configuration -- single-pass, dual-eye, collapse, double-wide, and native resolution -- on or off together, clears the census dry-run, and reloads the shaders either way (on to apply the patches, off to restore the pristine shaders). The label tracks the current state; the individual bring-up levers stay below.
Instrument the CreateVertexShader detour to classify every shader creation -- whether it arrived pre-substituted from CreateVertexProgram (pending) or was analyzed here -- into patched / already-cb13 / no-refs / errored buckets, and surface the tally, alongside the recorded-shader count and the CreateVertexProgram census, in the per-frame log, the screenshot JSON sidecar, and the debug UI. The detour also substitutes a patchable-but-unpatched blob in place, a defensive catch for any shader-creation path that skips CreateVertexProgram. In practice the shader bounce re-creates every shader through CreateVertexProgram (the census sees all of them), so it catches nothing today -- but this instrumentation is what proved that, and shows the character shaders fall in the no-refs family, which the clip-space reprojection pass will handle rather than the cb0 remap. Also install the COM detours before the trampoline creates the shader, so a shader patched at the blob level is recorded even when created before the detours' lazy first-frame install.
reproject_vertex_shader is the fallback for the baked-WVP / terrain / GPU-indirect families that have no per-eye cb0 operands (skinned characters, props, tree trunks, ...). Instead of remapping cb0, it renames the shader's own SV_Position writes to a temp rClip and post-multiplies by a per-eye M_eye before each ret, so o0 = M_eye · clip_center lands the shader's own centre-clip position in each eye. M_eye = VP_eye · VP_center-inverse rides in a four-rows-per-eye cb13 block after the 10 remap rows (STEREO_REPROJ_CB_ROWS = 18, addressed cb13[rBase + 10 + j] with rBase = 4*eye). It reuses the remap's whole scaffold: the SV_InstanceID input, SV_ViewportArrayIndex output, SFI0 bit, signature append, and checksum. The injection point is the first executable instruction, which means is_declaration has to cover the SM5 declaration block (0x91..=0xA2, dcl_function_table through dcl_resource_structured) and not only SM4's 0x58..=0x6A. Without it the SM5 declarations the terrain and vegetation shaders use classify as executable, and the prologue lands mid-declarations -- silently, on every structured-buffer shader. Shader-agnostic -- it works on any vertex shader that writes SV_Position from a scene view-projection, whatever buffer that came from. 225 of the corpus's 245 no-cb0 vertex shaders reproject cleanly (the other 20 write no position -- the terrain VS whose clip is built in the domain shader); real D3DDisassemble accepts the character shader with the exact expected idiom.
dir() and subdir() now return Option<Result<PathBuf, io::Error>> instead of swallowing mkdir failures via .ok()?. None still means the root is unavailable (DLL path unresolved); Err distinguishes a directory-creation failure so callers can report it rather than silently disabling. All call sites updated to and_then(|r| r.ok()) or match, and crash.rs now prints the io error to stderr at startup.
…h RAII The payload's detours hold raw pointers into engine state that is not safe to drop through, and unwinding across an FFI boundary (the detour trampolines) is UB. Set panic = "abort" in dev and release profiles; the panic hook in crash.rs still fires before the abort so crash logs are written. Gate the two should_panic tests on not(panic = "abort") since they abort the test process under that strategy. Add CritSecGuard (engine_context.rs) and RenderContextRestore (water.rs) so the engine's critical section and render-context matrices are restored on drop even if the closure unwinds. The guards keep the invariant honest if the panic strategy ever changes or a catch-unwind is introduced.
Replace the bare u32 foveal_first_pass and foveal_last_pass fields in FoveationConfig with the RenderPassId enum, with custom serde that serializes as the i32 discriminant so existing config files stay readable. Add a validate() method that checks the pass range and mask-bit power-of-two invariant, with FoveationConfigError and unit tests. Wire validation into foveation_plan so an invalid config disables foveation with a warning. Replace the debug UI DragValue with a ComboBox listing all scene render passes by hex value and engine debug name. Also fix two minor hover-text whitespace issues in the same file.
Replace the per-frame warning on transient mirror-present failures with a transition log (first failure), a periodic reminder (every 90 frames), and a recovery notice when the streak ends. Mirrors the pattern already used by frame_begin's failure-rate limiting.
The SinglePassConfig doc comments referenced the old single_pass_-prefixed accessor names (e.g. single_pass_water_uv_per_eye) that no longer exist after the config refactor. Update them to the current names (water_uv_per_eye, ssdecal_per_eye, ssao_per_eye, reconstruct_per_eye, clustered_per_eye).
…r terrain startup flag clustered_lighting: add state.ctx == ctx guard to the geometry-constant substitution so a cross-block upload with the same (cb_index, offset, count) shape that lands while the split is active is not mis-substituted. Mirrors the guard already present in substitute_assignment_view. terrain: set STARTUP_APPLY_DONE only after the patcher and engine-singleton checks succeed, so an early return retries the startup apply on the next frame instead of permanently skipping it. reconstruction and ss_decal: add safety-justification comments for the SetScissorEnable and staging-restore paths.
Move BYPASS_RESIZE_SUBSTITUTE.store(false) after the device-info restore so the flag covers the entire save-call-restore window. A concurrent resize substitute that reads device info while the bypass was prematurely cleared would see the new swapchain size instead of the original render size.
- wine-debug SKILL.md: .claude/ → .polytoken/ for the catch_crash.py path - CONTRIBUTING.md: fix 'cargo clippy -all' to 'cargo clippy --all' - docs/mod/hud.md: update the HUD composite-exclusion explanation to match the engine's actual back-buffer aliasing path - tools/shaders/README.md: point the wine-prefix instructions at the shared scripts/wine_prefix.sh provisioning rather than the old per-tool path
…r flag Pull the pyxis-defs mapping of the post-scene lighting seams (the exposure publish pair, the atmosphere block and its sky-lighting SH targets, the low-res particle compose, the render context's lighting fields) and adapt dof_no_reproject to the corrected flag name: bit 0 of the post render context's flags is the engine's camera-underwater bit, not a motion-vector reprojection toggle. Same bit, same validated behaviour, honest name.
The foveation fill-in is a raw fullscreen draw on the immediate context and never set a blend state, so it inherited whatever the engine's last block left bound. An inherited additive/alpha state made the fill add its neighbour-averaged reconstruction over the whole already-shaded frame (~2x brightness) instead of replacing the dropped peripheral pixels -- latched for as long as the scene kept handing it the same leftover state, and flipping with head pose. Bind opaque blend explicitly; the existing state backup still restores the engine's blend afterwards.
Instruments distilled from the foveation blend-state hunt, kept for future pipeline debugging (single-pass stereo especially): - MainColor mean brackets at the fixed pipeline stages (post-resolve, around the aerial-perspective composite, post-block entry, post-chain start), under diagnose_main_color_means -- bracketing a global change between two stages names the frame segment that injects it. - A per-pass MainColor mean ladder over the late-scene passes, under diagnose_pass_sweep, to walk a change to the exact pass. - FP/VP global-constant staging dumps at scene time and frame end, under diagnose_global_constants, for good-vs-bad frame diffing. - A one-shot auto-capture: arm a checkbox and a render trace starts the moment the frame brightness steps against its recent median, then disarms. The CPU histogram lags too far behind short flips to serve as the trigger, so this reads the MainColor mean directly at the post-chain seam.
The first in-headset validation caught the fill-in inheriting the engine's leftover blend state (the pose-latched ~2x global brightness step); note the finding and the generalized lesson in the doc, and stop describing the feature as wholly untested on hardware.
Sync pyxis-defs: 0x140_119_440 is ToneMappingEffect::Apply (publishes the applied exposure pair into the frame context and runs the exposure-weighted meter) and 0x140_119_8F0 is GenerateHistogramForFinalScene (the raw final-scene meter) -- the DrawHistogramWindow name belonged to a debug overlay. The applied exposure field at +0xBF0 is m_ExposureBrightPoint, and PostEffectRenderContext folds into RenderContext (one engine struct, one def). Rename the hooks and trace events to match, serve the eye-1 skip path's out-params through the typed exposure fields instead of raw word copies, and correct the docs that placed the HDR->LDR tonemap composite in "DrawHistogramWindow": the composite is CDepthOfFieldEffect::Apply, multiplying the scene by the published exposure (fragment constant c2.x).
Group docs/engine/ and docs/mod/ into mirrored subject subdirectories matching the payload's module map -- rendering/, performance/, character/ / body/, gameplay/ / input/, and mod/stereo/ for the single-pass family -- with single-doc subjects staying at the split's root. The engine-vs-mod nature split stays the top level per the documentation convention, which now records the subject layer too. All intra-doc links, code-comment references, and the index follow the moves; the rendering \xc2\xa7N anchors are untouched.
rt_hash had accreted the MainColor mean brackets, the per-pass ladder, the constant-staging dumps, and the brightness auto-capture alongside its actual job (per-eye render-target hashing and trace screenshots). Give the probes their own module, pipeline_probes, with the instrument catalogue in the module doc; rt_hash returns to hashing.
…ntions Pull in the conventions this style has since grown elsewhere: intra-crate paths anchor at crate:: (never super::, except a test module's use super::*), repeated or unwieldy paths get top-of-module imports, pub(in ...) reads as a smell, comments describe the present state, file splitting at the 1000-line threshold along concern seams, wide folders get subfolders, parking_lot::Mutex as the synchronous-lock default, .as_ref() / .as_mut() over manual double-derefs, and a testing section (tools, no serde-only tests, anonymized fixtures). Also correct the Logging note that still described another project's frontend.
Apply the newly adopted rule across the hand-written crates: every super::-anchored import and call path becomes its crate::-anchored equivalent, leaving use super::* in test modules (the sanctioned idiom) and pub(super) visibilities untouched.
A detour's signature mirrors the hooked function's ABI and cannot be bundled into a parameter struct, so the too_many_arguments allow is the honest annotation there -- the payload's four allows are all detours.
Hoist the function-local use statements to module tops, import the parent modules for the deep intra-crate call paths the crate:: sweep surfaced (module imports where item imports would collide on leaf names), and replace the one manual double-deref with the direct copy it was.
crash.rs sat at 1112 lines; split it along its natural seams into a folder module -- the install/VEH/panic-hook root, the allocation-free line writer, the memory probes and module resolution, the frame-phase breadcrumb ring, the exception record writer, and the stack/thread dumps. A move, not a rewrite: the public surface (install, uninstall, Phase, mark) is unchanged and no consumer needed edits.
profiler/gpu.rs sat at 1047 lines; split it into a folder module -- the seam/entry-point root, the timestamp-query ring, the starvation-gap subdivision, the puffin stream bridge, and the rolling summary. A move, not a rewrite: the public surface is unchanged and no consumer needed edits; the one structural change folds teardown's pool destruction into the profiler type so the pools stay private.
ui/render.rs sat at 1714 lines; split it into a folder module -- the capture machinery, and one section module per debug-UI area (stereo, far-field, stereo corrections, single-pass bring-up, reduced resolution, post-FX, and render-pass presentation), with the root composing the sections in the original order. A move, not a rewrite: bodies are verbatim, the public capture surface is unchanged, and sibling reach that pub(super) would have cut off at the new depth uses pub(in crate::ui).
vr/mod.rs sat at 1698 lines; split it into sibling files -- the VrState singleton and session lifecycle, the per-frame loop, session creation, the stereo swapchain, cross-inject persistence, the loader search, eye resolution, and recentering -- leaving the root as declarations, re-exports, and the two shared view constants. A move, not a rewrite: the public surface is byte-identical and no consumer needed edits.
clustered_lighting.rs sat at 1010 lines; split it into a folder module -- the DrawClustered detour and run driver, the per-eye froxel split state machine, the constant-upload interception detours, and the off-axis tile bounds derivation with its tests. Each detour stays beside the hook_library() that binds it, composed at the module root. A move, not a rewrite: the pub(crate) surface sibling hooks consume is unchanged.
hooks/graphics_engine sat at twenty flat entries; group the leaves into scene/ (world geometry and culling), screen/ (the depth-reconstruction basis and its screen-space/fullscreen consumers), and post/ (the post chain and metering), with the engine-core seams staying at the root. Each group composes its children's hook libraries, and the root re-exports the externally referenced leaves so every existing graphics_engine::<leaf> path resolves unchanged.
The crate:: sweep left same-crate imports as separate use statements, and imports_granularity only merges within a contiguous group -- a blank line splits it. Adopt group_imports = "StdExternalCrate" (nightly-only, like the granularity option already in use) so rustfmt owns the std/external/crate grouping and the merge, and reformat: 30 files had split or misplaced groups; the rest were already canonical.
The crate:: sweep left repeated fully-qualified call paths inline; the import-once rule wants them imported at the module top. Hoist each repeated path -- functions and statics as item imports, associated functions at their type, with a parent-module import where the leaf would collide -- and let rustfmt fold the new lines into the existing groups. The handful of paths left qualified collide with same-named locals or fields, where the qualified form is the clearer one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Rendering both eyes in one geometry walk into a double-wide target, by rewriting the game's vertex-shader bytecode in flight and routing draws into per-eye viewport halves. The motivation: the frame is draw-submission-bound — roughly 20k draws per frame at ~41% GPU utilisation, measured in
docs/mod/performance/performance.md— so halving the walk is the lever that matters.Single-pass stereo is incomplete and ships default-off. The master switch (
stereo.single_pass.enabled) is off; nothing in the feature runs without it, and the ten COM detours it needs are never installed. It's further gated behind a viewport-routing capability probe that falls back to the double-draw path. This branch merges as a foundation to resume from, not a finished feature.What's actually finished
A good deal of the branch isn't SPS and is ready to use:
dxbc-stereo— a standalone, platform-independent DXBC parser and rewriter with four shader transforms (cb13 remap, clip-space reprojection, terrain tessellation eye-threading, SS-decal UV separation), validated against all 455 vertex shaders in the shipped bundle and unit-tested natively.vr.own_back_buffer, default on) — the scene renders at per-eye VR resolution while the DXGI swapchain stays at window size, so the present stops rescaling every frame. The ownership handoff and free-ordering invariants live invr/back_buffer.rs.vr.mirror_framingFill/Fit, plusvr.mirror_zoom). The egui overlay tracks the live window size, so the UI no longer stretches once render and window sizes diverge. Mirror-present failures distinguish transient from fatal instead of disabling on the first error.sessions/<timestamp>/instead of loose files beside the DLL.panic = "abort"workspace-wide: unwinding across a detour trampoline is UB.relax_terrain_patch_hull_culls(default on in VR), not yet confirmed in-headset.Fixed along the way
Known-incomplete, deliberately
The four documented gaps are in
docs/mod/stereo/single-pass-stereo.mdunder "known gaps". The largest: the legacy-water intercept may be unreachable, and confirming that needs one in-headset session with the decline diagnostic this branch adds. Several of the ~25 single-pass sub-flags are blind-implemented and unvalidated (occluder, for one); each flag's doc names the specific artifact it addresses.Tooling and hygiene
dxbc-tool, a Rust compile/disasm CLI, replaces the old C shader harnesses. One shared, disposable wine prefix serves all Linux-side Windows tooling;xwin_test.shruns the cross-compiled unit tests under wine; the OpenXR loader is fetched pinned and checksum-verified.imports_granularity. Stable rustfmt silently no-ops on it, so past runs false-passed; the check runs on nightly now.Reviewer notes
jc3gi/srcis entirely generated — review thepyxis-defssubmodule pin, not the lines.