WIP: fix(vk): a draw left out after it took its occlusion query no longer reports "hidden" #253

Draft
jarynclouatre wants to merge 1 commit from jarynclouatre/Longshot:fix/occlusion-skipped-draw into main
Contributor

Why

A draw that is left out partway through draw() could tell the game its object is hidden. With the occlusion counts of #197, the game leaves out of its draw lists the objects whose report reads 0. A draw whose query was begun and that then returned before its draw command (descriptors that could not be bound, an index buffer or the indirect records not found, a negative count from the game) ended a query that had counted nothing: available, with a count of 0. The object was then left out though nothing had tested it. The rule for such a draw is "visible", as the comment at the guard says; it only held for a draw that returned before the query was begun.

What changed

  • vk (gpu/vk/vk_draw.cpp, draw): OccGuard no longer begins the query where it is made. It has a begin() that begins it once, and occ_guard.begin() stands before each vkCmdDraw* of draw() (nine places covering its thirteen draw commands). The destructor ends the query only if it was begun. A draw that takes any SKIP_HERE() after the guard leaves its query unused, and occ_collect (gpu/vk/vk_replay.cpp, unchanged) keeps its report "visible" once the submission is done.
  • tests (tests/test_vk_occlusion_queries.py): the guard is cut from vk_draw.cpp and run against the ring with spies for the two query commands. A draw that reaches its command begins once (asked twice) and ends; one that does not begins nothing, and the report of the run stays 4096. The source check now reads draw(): no vkCmdBeginQuery after the guard, every draw command has occ_guard.begin() ahead of it, and nothing returns between the two.

Flags and settings

None.

Measurements

main: fbd42024 (2026-11-13)
this PR: fix/occlusion-skipped-draw @ b3075e9f
Hardware: none used (game-free checks on a Linux x86-64 PC, g++ 15, Python 3.14.4); the Windows back end cross-built with llvm-mingw 20261104
Scene: none, the game was not run yet
Method: python3 -I -B tests/run_all.py, two runs each, interleaved (main, PR, main, PR), both runs of a side gave the same counts; this PR's tests/test_vk_occlusion_queries.py also run against main's sources
How to reproduce: the commands above in a checkout of each commit

main @ fbd42024 this PR @ b3075e9f
tests/run_all.py (run 1, run 2) 449 passed, 1 failed, 79 skipped (both) 449 passed, 1 failed, 79 skipped (both)
failing check test_bake_portability.py the same one
this PR's test_vk_occlusion_queries.py fails (the guard begins the query where it is made) passes
longshot_vk.dll for Windows (RX_VK_ONLY=1 RX_RELEASE=1 nx2/build.py --standalone) not built builds, 0 failed
suite main run 1: 449 passed, 1 failed, 79 skipped in 71 s (linux, 16 at once); failing: tests/test_bake_portability.py
suite PR   run 1: 449 passed, 1 failed, 79 skipped in 70 s (linux, 16 at once); failing: tests/test_bake_portability.py
suite main run 2: 449 passed, 1 failed, 79 skipped in 70 s (linux, 16 at once); failing: tests/test_bake_portability.py
suite PR   run 2: 449 passed, 1 failed, 79 skipped in 69 s (linux, 16 at once); failing: tests/test_bake_portability.py
PASS: occlusion counts: summed once their draws are done, visible when unknown (a draw left out never begins its query), overflowed, with a colour draw or reported again, in order, read and reset in one batch (two across the ring's end), the dry pass numbering as the workers do

test_bake_portability.py fails here for a missing Python module (zstandard), on main as well.

Testing

Tested: the game-free suite on a Linux PC, as in Measurements. The Windows back end builds from this branch. The Linux back end was not built here yet.

Not tested: the game itself. What a run has to show: with the default occlusion queries, the log's occlusion line (reports with a count, with 0, unknown) is the same on main and on this PR in a scene where no draw is left out, and the picture is the same. This stays work in progress until that run is done on Windows 11 (RTX 4070 Ti SUPER); its result goes in a comment here. I have no scene in which a draw is left out after its query on demand, so that path is covered by the fixture only.

Needs other testers: None.

Screenshots

Nothing on screen.

Risk and rollback

The query now covers the draw command alone, not the binds before it; only draw commands produce samples, so the counts are the same. A draw command added to draw() later without occ_guard.begin() would leave its query unused, which costs culling for that report and never an object; the source check fails on it. RX_VK_OCC_QUERIES=0 turns the occlusion counts off as before, and reverting the commit puts the old guard back.

Reviewer notes

  • Found while reading #197's code, not from a player's report: I know of no missing object traced to this.
  • git diff main...HEAD -- gpu/vk/vk_draw.cpp is the guard and nine one-line additions.

Issues

None.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths.
## Why A draw that is left out partway through `draw()` could tell the game its object is hidden. With the occlusion counts of #197, the game leaves out of its draw lists the objects whose report reads 0. A draw whose query was begun and that then returned before its draw command (descriptors that could not be bound, an index buffer or the indirect records not found, a negative count from the game) ended a query that had counted nothing: available, with a count of 0. The object was then left out though nothing had tested it. The rule for such a draw is "visible", as the comment at the guard says; it only held for a draw that returned before the query was begun. ## What changed - vk (`gpu/vk/vk_draw.cpp`, `draw`): `OccGuard` no longer begins the query where it is made. It has a `begin()` that begins it once, and `occ_guard.begin()` stands before each `vkCmdDraw*` of `draw()` (nine places covering its thirteen draw commands). The destructor ends the query only if it was begun. A draw that takes any `SKIP_HERE()` after the guard leaves its query unused, and `occ_collect` (`gpu/vk/vk_replay.cpp`, unchanged) keeps its report "visible" once the submission is done. - tests (`tests/test_vk_occlusion_queries.py`): the guard is cut from `vk_draw.cpp` and run against the ring with spies for the two query commands. A draw that reaches its command begins once (asked twice) and ends; one that does not begins nothing, and the report of the run stays 4096. The source check now reads `draw()`: no `vkCmdBeginQuery` after the guard, every draw command has `occ_guard.begin()` ahead of it, and nothing returns between the two. ## Flags and settings None. ## Measurements main: fbd42024 (2026-11-13) this PR: fix/occlusion-skipped-draw @ b3075e9f Hardware: none used (game-free checks on a Linux x86-64 PC, g++ 15, Python 3.14.4); the Windows back end cross-built with llvm-mingw 20261104 Scene: none, the game was not run yet Method: `python3 -I -B tests/run_all.py`, two runs each, interleaved (main, PR, main, PR), both runs of a side gave the same counts; this PR's `tests/test_vk_occlusion_queries.py` also run against main's sources How to reproduce: the commands above in a checkout of each commit | | main @ fbd42024 | this PR @ b3075e9f | |---|---|---| | `tests/run_all.py` (run 1, run 2) | 449 passed, 1 failed, 79 skipped (both) | 449 passed, 1 failed, 79 skipped (both) | | failing check | `test_bake_portability.py` | the same one | | this PR's `test_vk_occlusion_queries.py` | fails (the guard begins the query where it is made) | passes | | `longshot_vk.dll` for Windows (`RX_VK_ONLY=1 RX_RELEASE=1 nx2/build.py --standalone`) | not built | builds, 0 failed | ``` suite main run 1: 449 passed, 1 failed, 79 skipped in 71 s (linux, 16 at once); failing: tests/test_bake_portability.py suite PR run 1: 449 passed, 1 failed, 79 skipped in 70 s (linux, 16 at once); failing: tests/test_bake_portability.py suite main run 2: 449 passed, 1 failed, 79 skipped in 70 s (linux, 16 at once); failing: tests/test_bake_portability.py suite PR run 2: 449 passed, 1 failed, 79 skipped in 69 s (linux, 16 at once); failing: tests/test_bake_portability.py PASS: occlusion counts: summed once their draws are done, visible when unknown (a draw left out never begins its query), overflowed, with a colour draw or reported again, in order, read and reset in one batch (two across the ring's end), the dry pass numbering as the workers do ``` `test_bake_portability.py` fails here for a missing Python module (zstandard), on main as well. ## Testing **Tested:** the game-free suite on a Linux PC, as in Measurements. The Windows back end builds from this branch. The Linux back end was not built here yet. **Not tested:** the game itself. What a run has to show: with the default occlusion queries, the log's occlusion line (reports with a count, with 0, unknown) is the same on main and on this PR in a scene where no draw is left out, and the picture is the same. This stays work in progress until that run is done on Windows 11 (RTX 4070 Ti SUPER); its result goes in a comment here. I have no scene in which a draw is left out after its query on demand, so that path is covered by the fixture only. **Needs other testers:** None. ## Screenshots Nothing on screen. ## Risk and rollback The query now covers the draw command alone, not the binds before it; only draw commands produce samples, so the counts are the same. A draw command added to `draw()` later without `occ_guard.begin()` would leave its query unused, which costs culling for that report and never an object; the source check fails on it. `RX_VK_OCC_QUERIES=0` turns the occlusion counts off as before, and reverting the commit puts the old guard back. ## Reviewer notes - Found while reading #197's code, not from a player's report: I know of no missing object traced to this. - `git diff main...HEAD -- gpu/vk/vk_draw.cpp` is the guard and nine one-line additions. ## Issues None. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths.
fix(vk): a draw left out after it took its occlusion query no longer reports "hidden"
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 3s
b3075e9f65
draw() began the draw's occlusion query as soon as its pass was begun, and
ended it when draw() returned. A draw left out further down (descriptors that
could not be bound, an index buffer or the indirect records not found, a
count the game gave as negative) returned through the same guard, so its
query ended having counted nothing. The query was then available with a
count of 0, the report's sum could come to 0, and the game left the object
out of its draw lists though nothing had tested it. The collector's rule for
an unknown draw, "visible", only covers a query that was never begun.

The guard now begins the query at the draw command: occ_guard.begin() stands
before each vkCmdDraw* of draw(), with no way out of the function between
the two. A draw left out on the way never begins its query, and its report
stays "visible" as the comment at the guard already promised.

tests/test_vk_occlusion_queries.py runs the guard cut from vk_draw.cpp
against the ring: a draw that reaches its command begins once and ends, one
that does not begins nothing and the report stays 4096. It also reads
draw(): every draw command has its begin, and nothing returns in between.
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 3s
This pull request has changes conflicting with the target branch.
  • gpu/vk/vk_draw.cpp
  • tests/test_vk_occlusion_queries.py
Some workflows are waiting to be reviewed.
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/occlusion-skipped-draw:jarynclouatre-fix/occlusion-skipped-draw'
git switch 'jarynclouatre-fix/occlusion-skipped-draw'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!253
No description provided.