WIP: fix(vk): a direct quad draw's BaseVertex is its first vertex, as for every other non-indexed draw #907
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/tooling
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8015c9a
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
risk
high
risk
low
risk
medium
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!907
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:fix/direct-quad-basevertex"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Type: fix · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max (game-free checks only, arm64 and x86_64 under Rosetta) · Needs: Windows and Linux, any GPU
Why
Vulkan has no quad topology, so LS draws a direct quad draw as an indexed triangle list it writes into the recording's
stream. For a draw that is not indexed, those indices were the vertex numbers themselves (first vertex + 4q + corner), drawn
with a vertex offset of 0. Vulkan's
BaseVertexfor an indexed draw is its vertex offset, so a vertex program that reads thedraw's base vertex got 0 in such a quad draw. (A program reads it from the vertex stage's driver bank at
0x640, which LSreads as
BaseVertex.)Every other non-indexed draw LS makes is a
vkCmdDraw. Vulkan gives a non-indexed draw its first vertex asBaseVertex, sothose draws read the first vertex. The indirect quad draws in #851 read it too. Only direct non-indexed quad draws read 0, so
the same program got a different base vertex depending on whether its draw was quads.
Not seen in a game: in the macOS port's game runs no quad was drawn (their quad census line never appeared).
Found while making the indirect quad draws (now in #851), whose non-indexed records carry the first vertex as their vertex offset.
The issue tracker's titles (snapshot of 2026-11-14 20:01) have nothing that fits. See #420.
How to reproduce
Seen on: not seen in a game; shown on main's own quad branch by this PR's game-free test, on macOS 26.7.1, Apple M4 Max, arm64 and x86_64 under Rosetta (the code is the same on every OS)
Settings and switches: none. Any program that reads the base vertex, in a direct quad draw that is not indexed and does not start at vertex 0
Steps: from this branch, run
python -I -B tests/test_quad_base_vertex.py --cxx c++ --source <a checkout of main>. In a game: a directNVN_DRAW_PRIMITIVE_QUADSdraw with a first vertex other than 0, whose vertex program reads the base vertex (longshot_log.txt names such programs:vk: program … reads the draw's base vertex)Expected:
BaseVertexis the draw's first vertex, as in every other non-indexed draw LS makes (Vulkan's rule for a non-indexed draw)Actual:
BaseVertexis 0. The test reportsFAIL BaseVertex: not indexed count 4 first 1 …: got 0, want 1(576 failures of 5,936 checks)What changed
A direct quad draw that is not indexed now writes its triangles counting from 0 (4q + corner) and passes its first vertex
as the indexed draw's vertex offset. Vulkan fetches vertex index + vertex offset, so every vertex fetched is the same as
before.
VertexIndexis the same too, and LS reads it as NVN's vertex id. OnlyBaseVertexchanges, from 0 to the firstvertex. Indexed quad draws and every other draw are unchanged.
gpu/vk/vk_draw.cpp, draw()'s quad branch (main:7814-7844):4q + kinstead offirst + 4q + k(:7833-7834);vkCmdDrawIndexed's vertex offset isd.indexed ? d.base_vertex : d.first(:7840, was… : 0).{6⌊V/4⌋, I, 0, fv, fi}over a pattern of4q + corner),so direct and indirect quads give the same
BaseVertex.tests/test_quad_base_vertex.py(new):vk_draw.cpp(andDrawArgsout ofvk_draw.h) and runs it on the CPU, withstand-ins for the stream, the guest's index buffer and the commands.
BaseVertex= vertexoffset.
BaseVertex, the instances and the first instance, and that fewer than 4 or morethan 2^24 vertices draw nothing.
--sourceruns it on another tree.Measurements
Verdict: nothing to measure. The draw writes the same number of indices into the stream and records the same single command; only the values of the vertex offset and the indices change.
Flags and settings
None
Testing
Tested: game-free, on macOS 26.7.1, Apple M4 Max, at
daeb117con mainaa95d35f. This PR's head98218916is the same patch rebased onto main7bacf846(git range-diff: identical), and its tree is exactly the merge ofdaeb117cwith7bacf846on which the new test was run again and passed:tests/test_quad_base_vertex.py(new), arm64 and x86_64 under Rosetta (clang++ -arch x86_64):aa95d35f'svk_draw.cpp(--source): 576 failures on both, every oneBaseVertex(0 where the first vertex is not 0); main's vertices are right;BaseVertexand the vertices fetched), and the old indices with the first vertex as the offset (576 failures: each vertex fetched with the first vertex added twice).gpu/vk/vk_draw.cppcompiled whole, for arm64 and x86_64, with nx2/build.py's own macOS commands (in a tree with the macOS build, #845's head, whosevk_draw.cppandvk_draw.hare main's): main's file and this PR's both exit 0 on both. The objects reference and define the same symbols as main's (nm).python -I -B tests/run_all.py --jobs 6on this Mac, where main's runner takes macOS for Linux: 544 passed, 33 failed, 17 skipped. Mainaa95d35fgives 543 passed, 33 failed, 17 skipped, and by name every result is the same apart from the new test (pass). The 33 fail on main on this Mac too: test_appimage_package_retry, test_appimage_player_build, test_collect_wide, test_control, test_cpm_discovery, test_crash_dialog, test_crash_record, test_env_on, test_game_layout, test_lift_cleanup_error, test_linux_preflight, test_longshot_ctl, test_low_memory_notice, test_make_game_cache, test_mem_gate, test_mem_gate_children, test_mirror_trim, test_oot_state_tap, test_options_budget_once, test_options_path, test_pcache_header_read, test_pipeline_cache_file, test_pipeline_cache_save, test_pipeline_play_cache, test_pipeline_tour, test_prof_symbols, test_program_cache_cleanup, test_program_cache_one_variant, test_readback_late, test_report_run, test_split_giants, test_vk_pgo, test_walk_key.78ec1c47and63fc3d2c, arm64): novk: quad drawsline, whichappears at the first quad draw. In the scenes played no quad was drawn, so this change draws no pixel differently there.
These logs can't say whether any program reads the base vertex:
vk: program … reads the draw'scomes only from programstranslated during the run, and each run loaded a program cache of 47,798 keys.
Not tested: in the game, on any platform: no scene is known to draw a quad whose program reads the base vertex.
Not built on Windows (llvm-mingw) or Linux: no PC here. The change has no platform branch, so it runs the same on both.
Must be tested:
98218916) and main (7bacf846) the same way, runpython -I -B tests/run_all.pyon each, then start each to the title, expectingtest_quad_base_vertexto pass, no other check to change status against main, and the samevk:lines in longshot_log.txt as main's (by hand: test request T-P4YP)Risks
What could break: a program that reads the base vertex in a direct non-indexed quad draw now gets the first vertex
instead of 0, as it already does in every other non-indexed draw. The vertices fetched are the same.
What NVN itself gives a non-indexed draw there is not known, and this PR does not settle it:
gl_BaseVertex0 to a draw with no base vertex parameter, such as DrawArrays(ARB_shader_draw_parameters: "In
the case where the command has no parameter, the value of <gl_BaseVertexARB> is zero").
0x640; only the indexed macros put the base vertexthere (
src/video_core/macro.cpp,HLE_DrawArraysIndirect, lines 86 and 116).LS reads
0x640asBaseVertexin every draw (gpu/vk/vk_shader.cpp:540). If NVN's value for a non-indexed draw is 0 orthe base instance, that is a question about every non-indexed draw LS makes, not only quads. This PR only makes quads give
the same value as the rest.
A first vertex of 2^31 or more (not a real draw) makes the vertex offset negative; on 32-bit arithmetic the vertex index
wraps to the same value as before.
Turning it off: revert the commit
Compatibility: Nothing changes: no cache, save,
longshot_options.inikey or vk ABI is touched.Reviewer notes
git merge-tree, heads of 13:58 CST), each one hunk invk_draw.cpp's quad branch:mac/topic-draw-path-quads-mesh, which carries the indirect quads) and #852 (mac/topic-mac-perf, on top of it)write the triangles through
quad_draws::triangles(…, [&](uint32_t i) { return d.indexed ? src[i] : first + i; }, out)and draw through
drawid_direct::indexed(quads * 6, …, d.indexed ? d.base_vertex : 0, …). Resolution:d.indexed ? src[i] : iin the lambda (droppingfirst), andd.firstin place of the0.mac/topic-draw-path) draws through the samedrawid_direct::indexed(…, d.indexed ? d.base_vertex : 0, …). Putd.firstin place of the0; its corners merge to this PR's.tests/test_quad_base_vertex.pystand-ins for the names those branches use in thequad branch (
quad_draws.h,drawid_direct,drawid_indirect,quad_records,quad_tally,quad_lost,quad_rotated,quad_drawn,census_draw,provoking_note,census_fan_draw).fix/quad-stream-grow) merge cleanly. The test passes on the tree merged with #853 (itsstream_allocstand-in takes the third argument).7bacf846(head98218916, the same patch); the test passes on that tree. The whole suite ran atdaeb117conaa95d35f; on7bacf846it is queued and this description gets its result.Issues
See #420
Checklist
Everyone
type(scope): description) of the status line's type.main(7bacf846, head98218916).python -I -B tests/run_all.pypasses (any failure that also fails onmainis named).third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.mainand passing here.tests/that reads changed source updated in this PR, not weakened. (None needed a change: every check that readsvk_draw.cppgives the same result as on main; the new test reads the quad branch.)View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.