-
Notifications
You must be signed in to change notification settings - Fork 26
feat(mesh): add vs-in stage for input-assembler mesh export (#224) #227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
94cf4e1
feat(mesh): add vs-in stage for input-assembler mesh export (#224)
BANANASJIM 09b5081
test(mesh): exercise set_mesh_data helper; fix tasks.md verification …
BANANASJIM 21ba08d
fix(mesh): apply baseVertex + position offset for vs-in; warn on non-…
BANANASJIM 70d79a8
Merge branch 'master' into feat/224-vsin-mesh
BANANASJIM File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
95 changes: 95 additions & 0 deletions
95
openspec/changes/2026-05-15-issue-224-vsin-mesh/proposal.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| # OpenSpec: issue-224-vsin-mesh | ||
|
|
||
| ## Summary | ||
|
|
||
| Expose vertex shader input (VS-In) mesh geometry through the existing `rdc mesh` CLI command | ||
| and the underlying daemon mesh handler. | ||
|
|
||
| ## Motivation | ||
|
|
||
| The `rdc mesh` command currently supports `--stage vs-out` and `--stage gs-out` but not | ||
| `--stage vs-in`. VS-In corresponds to `MeshDataStage.VSIn = 0` in the RenderDoc API and | ||
| represents the raw per-vertex data fed to the Input Assembler — the most common geometry | ||
| inspection target for artists and tools debugging vertex attribute issues. | ||
|
|
||
| This change completes part of the unshipped geometry export plan from archived | ||
| `openspec/changes/archive/2026-02-19-phase2-buffer-decode`. | ||
|
|
||
| **Premise correction (adversarial review, 2026-05-15).** An earlier draft of this | ||
| proposal asserted that `_handle_mesh_data` and the OBJ-export path were "already generic; | ||
| only the stage map and the CLI option list need extending" and that "no changes needed | ||
| below the map". **That premise is RETRACTED.** Cross-checking against RenderDoc's official | ||
| `decode_mesh.py` reference revealed three correctness defects that are masked for | ||
| `vs-out`/`gs-out` (where `baseVertex == 0` and the position attribute is element 0) but | ||
| break `vs-in`: | ||
|
|
||
| 1. `mesh.baseVertex` was never applied to decoded indices. RenderDoc's reference adds | ||
| `mesh.baseVertex` to every index. For base-vertex `vs-in` draws the decoded mesh was | ||
| wrong; for `vs-out`/`gs-out` it is a no-op (`baseVertex == 0`). | ||
| 2. The position was read at `i*stride` (start of the interleaved vertex) instead of | ||
| `i*stride + mesh.vertexByteOffset`, so `vs-in` positions were wrong whenever POSITION | ||
| is not the first element of the vertex. | ||
| 3. Index-buffer width is honored from `mesh.indexByteStride` (16-bit vs 32-bit), matching | ||
| the reference; this is now covered by explicit regression tests. | ||
|
|
||
| The fix applies `baseVertex` and `vertexByteOffset` uniformly (not stage-special-cased), | ||
| exactly as the reference does, with a regression test asserting `vs-out` behavior is | ||
| unchanged. | ||
|
|
||
| ## Design | ||
|
|
||
| ### Daemon layer | ||
|
|
||
| `_MESH_STAGE_MAP` in `buffer.py` maps CLI stage strings to `MeshDataStage` integer values. | ||
| Add `"vs-in": 0` alongside the existing `"vs-out": 1` and `"gs-out": 2` entries. | ||
|
|
||
| The decode path calls `GetPostVSData(instance, view, stage_int)` and decodes the returned | ||
| `MeshFormat`. The decoder is corrected to match RenderDoc's `decode_mesh.py` reference: | ||
| indices are offset by `mesh.baseVertex`, the position attribute is read at | ||
| `i*stride + mesh.vertexByteOffset`, and the index width follows `mesh.indexByteStride`. | ||
| These corrections are applied uniformly across all stages (no stage special-casing); they | ||
| are no-ops for `vs-out`/`gs-out` where `baseVertex == 0` and the position is element 0. | ||
|
|
||
| The error string at the invalid-stage guard (~buffer.py:283) currently reads | ||
| `"invalid stage <name>; use vs-out or gs-out"`. This change must update that string to | ||
| include `vs-in` so callers see a consistent list of valid values. | ||
|
|
||
| ### CLI layer | ||
|
|
||
| The `--stage` Click `Choice` in `mesh_cmd` (`mesh.py`) hardcodes `["vs-out", "gs-out"]`. | ||
| Add `"vs-in"` as a valid choice. No other CLI logic changes. | ||
|
|
||
| ## Risks and Limitations | ||
|
|
||
| - **Non-draw events**: `GetPostVSData(VSIn)` returns an empty `MeshFormat` (zero | ||
| `vertexResourceId` / `vertexByteStride`) for compute or non-draw events. The existing | ||
| guard at ~buffer.py:293 returns JSON-RPC error `-32001` `"no PostVS data at this event"` | ||
| in this case — identical to the contract `vs-out` and `gs-out` already have. No | ||
| silent-empty path exists; behavior is consistent across all three stages. | ||
|
|
||
| ### What vs-in supports | ||
|
|
||
| - **Position geometry**: the single position attribute described by `mesh.format`, | ||
| located at `mesh.vertexByteOffset` within the interleaved vertex stride. Decoded | ||
| positions are `vertexByteOffset`-correct. | ||
| - **Triangle connectivity**: `TriangleList` / `TriangleStrip` / `TriangleFan`, with | ||
| indices that are `baseVertex`-correct (RenderDoc `decode_mesh` parity). | ||
|
|
||
| ### Known limitations (vs-in) | ||
|
|
||
| - **Only the position attribute is exported**, not full per-attribute IA. Other vertex | ||
| attributes (normals, UVs, colors) are not decoded. The lower-level fallback | ||
| (`GetVertexInputs` / `GetVBuffers` / `GetIBuffer`) for full per-attribute IA decoding is | ||
| explicitly out of scope for this change. | ||
| - **Packed / non-float position formats are unsupported**: the decoder only handles | ||
| 1/2/4-byte float-style components; packed formats (e.g. `R10G10B10A2`, SNORM/UNORM | ||
| integer-packed) are not decoded. | ||
| - **Non-triangle topology exports vertices only**: `LineList`, `PointList`, patch and | ||
| adjacency topologies produce no OBJ faces. Rather than silently emitting an empty/ | ||
| face-less OBJ, the CLI now prints a clear stderr warning naming the topology | ||
| (e.g. `mesh: topology 'PatchList_3' has no OBJ face mapping; exported N vertices, | ||
| 0 faces`) so geometry loss is never silent. | ||
|
|
||
| - **D3D12 on Linux**: This development box runs Linux; D3D12 captures cannot be exercised | ||
| locally. Verification follows the established model: ship the change, reporter | ||
| (@Misaka-Mikoto-Tech) verifies against real D3D12 captures (same approach as PR #226). |
40 changes: 40 additions & 0 deletions
40
openspec/changes/2026-05-15-issue-224-vsin-mesh/specs/daemon/spec.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| ## ADDED Requirements | ||
|
|
||
| ### Requirement: VS-In Mesh Stage Support | ||
| The daemon SHALL accept `vs-in` as a valid mesh stage identifier in addition to the | ||
| existing `vs-out` and `gs-out` stages. | ||
|
|
||
| #### Scenario: VS-In Stage Accepted | ||
| - **WHEN** client requests `mesh_data` with `stage` equal to `"vs-in"` | ||
| - **THEN** daemon calls `GetPostVSData` with stage integer `0` (`MeshDataStage.VSIn`) | ||
| - **THEN** daemon decodes the returned `MeshFormat` using the existing geometry decode path | ||
| - **THEN** daemon returns vertex positions (and any available attributes) in OBJ format | ||
|
|
||
| #### Scenario: VS-In on Non-Draw Event | ||
| - **WHEN** client requests `mesh_data` with `stage` equal to `"vs-in"` for a non-draw event | ||
| (or any event where the IA/VSIn stage produced no data) | ||
| - **THEN** `GetPostVSData` returns a `MeshFormat` with zero `vertexResourceId` or zero `vertexByteStride` | ||
| - **THEN** daemon returns JSON-RPC error `-32001` with message `"no PostVS data at this event"` | ||
| - **NOTE** this is the same error contract `vs-out` and `gs-out` already have; no silent-empty | ||
| path exists | ||
|
|
||
| #### Scenario: Invalid Stage String | ||
| - **WHEN** client requests `mesh_data` with an unrecognized `stage` value | ||
| - **THEN** daemon returns an error response | ||
| - **AND** the error message SHALL list `vs-in`, `vs-out`, and `gs-out` as valid values | ||
| - **NOTE** the error string at ~buffer.py:283 currently omits `vs-in`; this change requires | ||
| updating it to `"invalid stage <name>; use vs-in, vs-out or gs-out"` | ||
|
|
||
| ## MODIFIED Requirements | ||
|
|
||
| ### Requirement: Mesh CLI Stage Option | ||
| The `rdc mesh` CLI command SHALL accept `vs-in` as a valid `--stage` argument. | ||
|
|
||
| #### Scenario: VS-In CLI Invocation | ||
| - **WHEN** user runs `rdc mesh <eid> --stage vs-in` | ||
| - **THEN** CLI forwards `stage: vs-in` to the daemon `mesh_data` handler | ||
| - **THEN** CLI writes the returned OBJ content to stdout | ||
|
|
||
| #### Scenario: Unknown Stage Rejected at CLI | ||
| - **WHEN** user supplies an unrecognized `--stage` value | ||
| - **THEN** Click validation rejects the input with exit code 2 before any daemon call is made |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # Tasks: issue-224-vsin-mesh | ||
|
|
||
| ## Phase A: Mock | ||
|
|
||
| - [ ] Add stage int `0` entry to `GetPostVSData` mock in `tests/mocks/mock_renderdoc.py` | ||
| with a minimal `MeshFormat` (position attribute, Triangle topology, ≥3 vertices) | ||
| - [ ] Verify `tests/integration/test_mock_api_sync.py` still passes after mock change | ||
|
|
||
| ## Phase B: Daemon handler | ||
|
|
||
| - [ ] Add `"vs-in": 0` to `_MESH_STAGE_MAP` in `src/rdc/handlers/buffer.py` | ||
| - [ ] Update the invalid-stage error string (~buffer.py:283) to include `vs-in` | ||
| - [ ] Extend `tests/unit/test_buffer_decode.py`: | ||
| - happy-path test for `vs-in` stage | ||
| - empty-MeshFormat test for non-draw eid at stage `0` | ||
| - invalid-stage error message includes `vs-in` | ||
| - [ ] Run `pixi run test tests/unit/test_buffer_decode.py` — all pass | ||
|
|
||
| ## Phase C: CLI | ||
|
|
||
| - [ ] Add `"vs-in"` to `--stage` Click `Choice` in `src/rdc/commands/mesh.py` | ||
| - [ ] Extend `tests/unit/test_mesh_commands.py` with `vs-in` forwarding case | ||
| - [ ] Run `pixi run test tests/unit/test_mesh_commands.py` — all pass | ||
|
|
||
| ## Phase D: Integration + verification | ||
|
|
||
| - [ ] Extend `tests/integration/test_daemon_handlers_real.py` with `@pytest.mark.gpu` | ||
| VS-In test analogous to `test_mesh_data_real` | ||
| - [ ] Run `pixi run lint && pixi run test` — all pass, coverage ≥ 80% | ||
| - [ ] Run GPU integration test against vkcube capture — passes | ||
| - [ ] Code review |
74 changes: 74 additions & 0 deletions
74
openspec/changes/2026-05-15-issue-224-vsin-mesh/test-plan.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # Test Plan: issue-224-vsin-mesh | ||
|
|
||
| ## Scope | ||
|
|
||
| ### In scope | ||
| - Daemon handler `_handle_mesh_data` accepting `vs-in` as a valid stage | ||
| - `GetPostVSData` mock keyed at stage int `0` in `mock_renderdoc.py` | ||
| - CLI `rdc mesh --stage vs-in` option acceptance and forwarding | ||
| - Integration test against a real Vulkan capture (vkcube) | ||
|
|
||
| ### Out of scope | ||
| - Multi-stream IA decoding | ||
| - Non-Triangle topology OBJ export | ||
| - D3D12 captures (Linux dev box; deferred to reporter verification) | ||
| - `GetVertexInputs` / `GetVBuffers` / `GetIBuffer` lower-level fallback | ||
|
|
||
| ## Test Matrix | ||
|
|
||
| | Layer | Test Type | File | | ||
| |-------|-----------|------| | ||
| | Unit | Handler: vs-in stage accepted, MeshFormat decoded | `tests/unit/test_buffer_decode.py` (extend) | | ||
| | Unit | Handler: invalid stage still errors with vs-in in message | `tests/unit/test_buffer_decode.py` (extend) | | ||
| | Unit | CLI: `--stage vs-in` forwarded to daemon | `tests/unit/test_mesh_commands.py` (extend) | | ||
| | Unit | CLI: `--stage` rejects unknown values | `tests/unit/test_mesh_commands.py` (extend) | | ||
| | Integration | Real Vulkan capture: vs-in OBJ export round-trip | `tests/integration/test_daemon_handlers_real.py` (extend) | | ||
|
|
||
| ## Cases | ||
|
|
||
| ### Handler: `_handle_mesh_data` with `vs-in` | ||
|
|
||
| - **Happy path**: request `{"stage": "vs-in"}` at a valid draw eid → `GetPostVSData` called | ||
| with stage int `0`; response contains vertex positions. | ||
| - **Empty MeshFormat (non-draw)**: mock returns `MeshFormat` with zero `vertexResourceId` | ||
| at stage `0` → handler returns JSON-RPC error `-32001` `"no PostVS data at this event"` | ||
| (same contract as `vs-out`/`gs-out`). | ||
| - **Invalid stage string**: request `{"stage": "hs-out"}` → error response; error text | ||
| includes `vs-in`, `vs-out`, `gs-out`. | ||
|
|
||
| ### Mock: `GetPostVSData` at stage 0 | ||
|
|
||
| - Mock in `tests/mocks/mock_renderdoc.py` stores per-stage `MeshFormat` keyed by stage int. | ||
| - Confirm stage key `0` is populated with a minimal `MeshFormat` (position attribute, | ||
| Triangle topology, at least 3 vertices). | ||
| - Confirm stage keys `1` and `2` are unaffected. | ||
|
|
||
| ### CLI: `--stage vs-in` | ||
|
|
||
| - Extend `test_mesh_stage_forwarded` in `tests/unit/test_mesh_commands.py`: | ||
| invoke `rdc mesh <eid> --stage vs-in` → assert daemon request contains `"stage": "vs-in"`. | ||
| - Invoke `rdc mesh <eid> --stage bad-stage` → assert Click validation error, exit code 2. | ||
| - Invoke `rdc mesh <eid>` (no `--stage`) → default behavior unchanged (existing test). | ||
|
|
||
| ### Integration (`@pytest.mark.gpu`) | ||
|
|
||
| - Analogous to `test_mesh_data_real` (~line 1573 in `test_daemon_handlers_real.py`). | ||
| - Open a vkcube Vulkan capture; pick a draw eid known to have vertex data. | ||
| - Call handler with `stage=vs-in`; assert OBJ output is non-empty and vertex count > 0. | ||
| - Assert OBJ contains `v ` lines; count matches reported vertex count from `MeshFormat`. | ||
|
|
||
| ## Assertions | ||
|
|
||
| - Exit code 0 on success, non-zero on error. | ||
| - VS-In response vertex count equals the value reported in the returned `MeshFormat`. | ||
| - OBJ vertex lines (`v x y z`) are well-formed floats. | ||
| - Error messages for invalid stage name appear on stderr and include all three valid stage | ||
| names (`vs-in`, `vs-out`, `gs-out`). | ||
| - Coverage gate: `pixi run test` overall coverage stays ≥ 80%. | ||
|
|
||
| ## Risks | ||
|
|
||
| - **Mock key alignment**: `GetPostVSData` mock currently uses stage int keys; confirm key | ||
| `0` is explicitly exercised or the test will silently pass against a wrong code path. | ||
| - **D3D12 gap**: integration test runs Vulkan only; D3D12 path is untested on this machine. | ||
| Mitigation: document in test as `# D3D12: verified by @Misaka-Mikoto-Tech on real capture`. | ||
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
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix code-span whitespace at Line 58.
vinside backticks includes a trailing space and can trigger markdownlint MD038. Rephrase to avoid space inside the code span (e.g., “lines starting withv”).🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 58-58: Spaces inside code span elements
(MD038, no-space-in-code)
🤖 Prompt for AI Agents