11 Commits
Author SHA1 Message Date
LibretroAdmin 8c2e5a410e Vulkan: make the fixed and utility pipelines on the pipeline thread
A renderer hands its general, modifier volume, prepass and other fixed
pipelines to the pipeline thread when it starts, and no longer waits for
them; a draw takes one made, makes it itself if the thread has not
started on it, or waits for the thread if it has. The quad, SSAA resolve,
motion vector, texture decoder, TA decoder and compute renderer
pipelines go the same way, and one the thread could not make is made
where it is first used, failing there as it did at start. The compute
renderer's kernels are handed over first, as every frame uses them.
2026-10-28 08:23:04 +01:00
LibretroAdmin 4fcbf561c8 vulkan: the depth is the position's, as the chip has it, in every pass
The raster renderers wrote a depth of their own from the fragment
shader, log2(1 + 100000/w)/34, in every pass, and tested EQUAL between
passes: the per-pixel renderer's colour pass over its depth pass, a
polygon drawn again for its shadow, the depth prepass of a sorted
translucent list under its colours. Each pass has its own fragment
shader and so its own pipeline, and a driver may round the same
expression differently in each one: where a pass's depth missed the
other's by a unit in the last place, the EQUAL test failed and the clear
colour showed through decals and shadow blobs as black specks. Only on
real GPUs - a software device compiles every pipeline alike - and only in
the raster renderers: the compute renderer derives a depth once. The
reference renderer (tools/rend/refsw) says how the chip has it: 1/w as a
32-bit float, interpolated by a plane equation in screen space, compared
once in the ISP; the layers of the scenes with the specks are thousands
of units apart, so they were never confused.

Now the depth is the vertex position's, by the fixed functions, and no
fragment shader writes one. A vertex goes out as (x*w, y*w, S, w) with
w = 1/z, so the depth the device interpolates is S*z: the chip's 1/w,
linear in screen space, in the D32 float depth buffer with larger nearer
(reversed Z, the viewport's depth range 0 to 1), the eight depth modes
the device's own compare operations as before. S is 1/fZ_max, the
parse's; a frame decoded on the GPU has fZ_max only on the GPU, so the
decode writes 1/fZ_max into a word of its frame (TAG_O_ZSCALE), and the
drawers copy that word over the host's into the vertex uniforms before
the render pass - nothing read back. The vertex uniforms are a small
buffer of their own for that, one an image, written before the render
pass begins (the main buffer's layout is only known after). The vertex
shaders clamp the position's z to w, so a z beyond fZ_max is at depth 1
and not clipped; the matrix's z scale (ExtraDepthScale) is undone for
the depth by a second uniform.

One text, shaders/position.glsl, places the vertices of every vertex
module (polygons, modifier volumes, the per-pixel renderer's), from the
same uniform block, and declares gl_Position invariant: the depth pass,
the colour pass, the shadow redraw, the prepass and the modifier volumes
are placed identically by construction, so EQUAL holds. Where a fragment
shader keeps a depth in the per-pixel lists or compares one (the
translucent modifier volumes, the lists' manual depth test) it is
gl_FragCoord.z, the same value the depth test took; fog keeps
gl_FragCoord.w. The modifier volume fragment shaders are empty but for
the final pass's tint. With no depth written by a fragment shader, early
depth testing works in every pass; on lavapipe the per-triangle
renderer's frames are 10-25% less GPU time, the per-pixel renderer's
10-35% (Jet Grind Radio, three scenes).

Ties stay as they were: draw order (opaque: later wins on >=; sorted
translucency: the stable sort), as refsw does.

raster_spv.inc made again (tools/rend/kernels_spv.sh), the GPU-side TA
kernels too for the new word; tools/pvr/ta_gpu_test compares the word
with the parse's 1/fZ_max.

tools/rend/depth_test.sh shows the mechanism on a device (lavapipe): a
floor drawn and drawn again by another pipeline with EQUAL misses no
pixel with the position's depth, nor with the old fragment depth while
both passes round alike, misses them all when the second pass's
fragment depth is a unit off (what the driver did), and misses many
when the second pass's position is a unit off (why the position is one
invariant text); and it reads the shipped modules: every polygon vertex
module declares its position invariant, none writes gl_FragDepth.

On lavapipe the pictures of Jet Grind Radio's select screen, train and
street scenes (per-pixel and per-triangle, TA processing on the CPU and
on the GPU, the validation layer on) are the same pixels as before: the
new depth orders every pixel as the old one did there, and so is as
close to refsw as before (per-pixel: mean 4.29, 3.11, 5.37; the compute
renderer 3.55, 2.92, 5.19). What changes on real GPUs - the specks - is
to be seen there.
2026-10-20 01:52:39 +02:00
LibretroAdmin a963f1f16d vulkan: the TA decode's frame words filled without two writes to one word
Each frame decoded on the GPU starts with its output words filled: all of
them with zero, then the first, fZ_max, with 1.0. Two transfer writes to
the same word with no barrier between them are not ordered: a GPU that
runs the two fills at once can leave fZ_max at zero. The Khronos
synchronization validation reports it on every frame
(SYNC-HAZARD-WRITE-AFTER-WRITE, vkCmdFillBuffer).

fZ_max is now filled alone and the zeros start at the word after it: the
same values, no word written twice, one fill command as before plus none.

Tested on lavapipe with synchronization validation on, Jet Grind Radio's
character select, TA processing on the GPU: the hazard was the only
message before, and there is none after.
2026-10-16 03:59:42 +02:00
LibretroAdmin 4ad3ad4316 pvr: the depth prepass leaves out on the GPU the strips no vertex makes opaque
GenPrepass() leaves out a polygon whose alpha is its vertices' (or
theirs times the texture's) when none of its vertices has 255
(PrepassVerticesOpaque()): no pixel of it can hide what is behind it.
The header pass of GPU-side TA processing has not got the vertices, so
its prepass drew such polygons and the shader discarded every pixel.

Now the CPU writes, after the prepass's indices in the sorted indices'
buffer, a check for each translucent strip in the prepass whose alpha
is its vertices' (PrepassAlphaFromVertices(), the test
PrepassVerticesOpaque() starts with): its vertices and where its
triangles are among the prepass's (TAG_T_PREP, TAG_PC_*). A new kernel
(TAG_K_PREPASS), dispatched after the unpacking and before anything
moves the colours, looks at each strip's vertices, a workgroup a strip,
and where none that is drawn has an alpha of 255 makes its triangles
(a, b, c) into (a, a, a), which draw nothing. The draws and their
counts stay the CPU's; nothing is read back. A translucent strip is a
polygon parameter of its own, so this is GenPrepass()'s choice exactly.

The sorted indices are written when the decode is recorded, before the
kernel that reads them (they were written when the frame was drawn).
Vulkan and OpenGL both: the buffer is bound to the kernel as set 1
binding 10 and binding 13.

tools/pvr/ta_gpu_test: GenPrepass() is given the vertices' own alphas
again; the checks are run on the device and by the model, which must
agree word for word, and the prepass's draws must be GenPrepass()'s as
before, a draw all of whose triangles were left out being one it has
not got. It counts the area of what is left out.

The prepass's area per sorted frame at 640 x 480 (ta_gpu_test, the
device's vertices): Jet Grind Radio s9 13 of 1390264 pixels left out,
s10 107714 of 583534 (18%), Virtua Fighter 4 29384 of 268756 (11%), Sega
Rally 2 15156 of 285594 (5%), Dead or Alive 2 none.
2026-10-16 02:43:46 +02:00
LibretroAdmin b5a3caa3ae vulkan: the depth prepass of sorted translucent polygons, GPU decode too
The prepass of rend/sorter.h for the Vulkan per-triangle renderer, by
triangle and by strip, and for frames whose vertices the GPU decodes and
whose triangles the CPU sorts (TA_GPU_SORT).

Its pipelines are all made with the render pass, never in the middle of
a frame: one fragment shader that works out a polygon's alpha as the
colour pass's does - the same expressions in the same precisions, with
what that one has as macros read from the push constants (where the
colour pass has its trilinear alpha, which no polygon of the prepass
needs) - made textured or not, flat or Gouraud, and at each SSAA rate;
pipelines for each of those and each culling, depth test and write on,
no colour writes. The push constants the colour pass left are put back
after it.

Pipelines made (PipelineManager::CreatePrepassPipelines(), for the
screen's and the render-to-texture pass): prepassPipelines[textured]
[Gouraud][no culling, front, back][per-sample rate], 12 without SSAA and
24 with, from 4 (8) prepass shaders (ShaderManager::GetPrepassShader()).
They are not made again when the way of sorting changes.

GPU decode: ta_gpu_prepass(), the sorted passes' triangles grouped as
GenPrepass() does, from their headers (no vertex alpha looked at: the
vertices are the GPU's), their indices after the sorted ones in the
decoder's room, which is twice what it was.
2026-10-15 18:53:29 +02:00
LibretroAdmin 35866c21d6 vulkan: the renderer and GPU-side TA processing set up between frames, not in the first one
The renderer was made in rend_frame(), on the first render after the
context was made, and GPU-side TA processing was set up in its Process()
for that same frame: about half a second of the main thread inside
retro_run() on lavapipe, most of it making the 14 pipelines, the rest
the kernels, the buffers and the texture kernels. With the option
switched on while a game ran, the same in the next frame drawn.

- retro_vk_context_reset() makes the renderer once the context is made
  (rend_prepare_renderer(), what rend_frame() did, which it still does
  where nothing has). The machine is not running there.
- GPU-side TA processing is set up with the renderer (BaseInit(), for its
  context and again for a change of size) and when the options change:
  retro_run() hands a change of options to the renderer between frames
  (Renderer::OptionsChanged()), before the machine runs, and makes a
  renderer the options have changed again there too. With the option
  off it is never made; where the device cannot do it, it is found out
  before a pipeline is made, as before.
- TaDecoder::BeginFrame() makes nothing: a frame it is not ready for is
  parsed on the CPU. A texture pool of another size takes it down.
- Setting it up is logged with the time it took.

On lavapipe (Jet Grind Radio, per-triangle, TA processing on the GPU),
the first frame drawn takes 0.24 s of retro_run() instead of 1.06 s; the
setup (0.85 s) is in the context reset, before the game's first frame.
2026-10-15 13:49:37 +02:00
LibretroAdmin 553edfcf75 vulkan: the TA decode's and the textures' kernels as SPIR-V made when the core is built
The GPU-side TA processing's 14 kernels and the 3 that make textures on
the GPU were compiled by the core itself, with glslang, from the text of
rend/ta_gpu.h and rend/texdecode.h, every time the renderer set them up:
30 to 110 ms of the main thread on lavapipe, on top of making the
pipelines.

They are SPIR-V now, made from the same headers by
tools/rend/kernels_spv.sh (glslangValidator -V -g0 --target-env
vulkan1.0: what the core's glslang made of them, SPIR-V 1.0 without the
names) and committed as core/rend/ta_gpu_spv.inc and
core/rend/texdecode_spv.inc, in place of the headers' text. The renderer
makes its shader modules from the words; nothing is compiled when the
core runs, and a build of the core needs no glslangValidator.

The files say which glslangValidator made them and the SHA-1 of each
kernel's text. "kernels_spv.sh check" compares those sums with the
headers as they are, which needs no glslangValidator, and with the same
glslangValidator every word too. tools/pvr/ta_gpu_test.sh and
tools/rend/texdecode_test.sh run it where they compared the text before,
their "update" makes both files again, and they compile the kernels they
test the same way, so that what they test is what the core has.

The pipelines go through the context's pipeline cache, as they did
(vulkan_pipeline.cache in the system directory's dc folder, loaded when
the context is made and saved when it goes).
2026-10-15 13:49:34 +02:00
LibretroAdmin 65295bfb80 vulkan: the per-triangle renderer's sorted passes and shadows from the GPU
With TA Processing = GPU, the per-triangle renderer now asks the header
pass for TA_GPU_SORT (unless translucent polygons are sorted by strip)
and TA_GPU_SHADOWS:

- Sorted passes: the CPU's sort writes its indices straight into the TA
  decoder's room for them (one slice a texture-pool slot, host-visible,
  made at init; a slot is written again only after BeginFrame() has
  waited for its fence, which the draws that read it were submitted
  with). DrawSorted() binds that slice as the index buffer and draws the
  runs as it draws GenSorted()'s, with the GPU's vertices, and binds the
  decode's indices back. No vertex or index of the frame is uploaded
  except the sorted indices themselves (12 bytes a triangle).
- Shadows: DrawModVols() takes the volumes' bounds from the header pass
  and scissors the second drawing to them as for a CPU frame, and
  DrawShadowed() draws each group by the second command TAG_K_SHADOW
  wrote, which has no indices where the group does not reach into the
  volumes.
- A frame decoded on the GPU with a sorted pass always draws it from the
  CPU's sort (it never falls to SortPParams(), which needs the CPU's
  vertices).

The decoder's command buffer doubles for the second commands, and the
decode records TAG_K_SHADOW after the draw commands. The per-pixel
renderer does not ask for either and is unchanged.
2026-10-15 13:49:19 +02:00
LibretroAdmin f153f936ec vulkan: both raster renderers draw from the GPU's TA decode
With TA Processing = GPU, a frame the GPU decodes is no longer also
parsed on the CPU: the header pass makes the render context ready to
draw in the full parse's place, and the per-triangle and per-pixel
renderers draw it from the decode's vertex, index and modifier volume
buffers with indirect draws whose counts the GPU wrote. Nothing is read
back, nothing is uploaded for it from the CPU, and nothing is allocated
or compiled while a game runs.

The header pass (ta_gpu_prepare(), TA_GPU_DRAW):
- The polygon parameter lists hold a draw group each, in the lists'
  order. For opaque and punch-through, a group is what make_index()
  joins, and its first is the slot of its draw command. For
  translucent, a group is a run of headers in a render pass that are
  drawn the same way; its strips stay separate strips, and its first is
  its place in the translucent draw records: a count word, then room
  for a draw a vertex. The render passes count the groups. The modifier
  volumes and the clipping rectangle are the header pass's; fZ_max is
  the GPU's, and the Vulkan renderers do not use it.
- A frame the full parse would drop for want of room is left to it,
  since only the CPU can keep the last picture on the screen. That
  covers vertices, modifier volume triangles and volumes exactly, and
  indices by at most four a vertex. Polygon parameters are bounded by a
  strip a vertex, or, where that is too many, by counting the strips'
  end bits, one word a vertex.
- Also left to it: frames the parse skips (frame skipping), the flat
  strips and the region table, and a parameter that comes where a strip
  has not ended (the parse takes it for a vertex, the decode for what it
  is). With TA_GPU_NO_SORT, so are frames that may have a sorted pass.

The decode (rend/ta_gpu.h) gets two kernels:
- trdraw: each translucent strip's record copied to its group's place,
  with the group's count, and its polygon parameter's words for the
  per-pixel renderer. With TA_GPU_INSTANCE, firstInstance is the
  strip's place in the list.
- provoke: the colours of a flat-shaded triangle's last vertex moved to
  its first, as SetProvokingVertices() does on the CPU, in its order of
  reads and writes.

The drawers (BaseDrawer::BindGeometry(), DrawIndices()):
- Opaque and punch-through groups: one vkCmdDrawIndexedIndirect() of
  the group's command.
- Translucent groups: one vkCmdDrawIndexedIndirectCountKHR() of the
  group's records. Without the extension, one multi-draw of the group's
  whole room, whose records past the count are zeroes. A device with
  neither keeps TA processing on the CPU. There are never more calls
  than the CPU path makes draws, and consecutive strips drawn the same
  way are one call.
- The per-pixel renderer reads the translucent polygon parameters from
  the GPU's buffer and numbers each strip by its draw's firstInstance.
  Without drawIndirectFirstInstance it takes TA_GPU_NO_SORT.
- The per-triangle renderer sorts translucent polygons on the CPU from
  the CPU's vertices (GenSorted(), SortPParams()). It takes
  TA_GPU_NO_SORT: a frame whose region array has any sorted pass is
  parsed and sorted on the CPU as ever. Sorting on the GPU is S1's.
- Shadows (the per-triangle renderer's second drawing of polygons in a
  modifier volume): the volumes' triangles are not on the CPU to bound,
  so every shadowed group is drawn again over the whole frame, and the
  stencil limits it to the same pixels.

The decode's outputs gain the translucent draw records (a vertex's worth
plus a header's of 20-byte records) and the translucent polygon words.
The decode's barrier waits for the fragment shaders as well, since the
per-pixel renderer reads those words there. The log gives why frames
were parsed on the CPU, and the pool's counts.

tools/pvr/ta_gpu_test also checks:
- the provoking vertex against the CPU's SetProvokingVertices();
- the translucent draw records and polygon words against the parse;
- the frame made ready to draw against the parse, group by group;
- the overrun verdict at the exact boundary, against the parse run with
  the same room and with one less.

On lavapipe with the validation layer: the T2 corpus byte for byte;
RetroArch screenshots the same with TA Processing CPU and GPU, both
renderers, threaded rendering on and off.
2026-10-15 13:49:07 +02:00
LibretroAdmin 846046a16b pvr: spare TA contexts for a full GPU pool, made with it
With TA Processing on the GPU, a frame's data goes into a slice of the
renderer's mapped buffer, and a slice is written again only once the GPU
work that read it is done. If every slice was taken, the frame got a
context of its own memory: taken from the spare contexts if there was
one, and otherwise allocated there and then, in the middle of a game.

Now nothing is allocated for that:
- Attaching the pool also makes one spare context of ordinary memory
  (or keeps one that is already spare), for the frame that finds every
  slice taken. That frame is drawn from the CPU's parse, as before.
- The renderer makes up to four more slices than frames in flight + 4,
  as many as a thirty-second of the memory budget left (vram_budget.h)
  allows.
- Only a game that writes to more frames at once than the slices and the
  spares together still gets a context allocated, as it would without
  the pool. That is counted, and said once in the log.

tactx_gpu_pool_stats() gives the two counts (frames that found the pool
full, contexts allocated) for the renderer's log.
2026-10-15 13:48:53 +02:00
LibretroAdmin ebbfd8211f vulkan: the TA decode on the GPU, behind TA Processing
A new core option, TA Processing (reicast_ta_processing: cpu, the
default, or gpu; Vulkan builds only), has the Vulkan renderers decode
each frame's tile accelerator data on the GPU (rend/ta_gpu.h).

- At its first frame with the option on, TaDecoder makes everything: a
  pool of TA context slices (frames in flight + 4) in one persistently
  mapped buffer of host-cached memory, given to the TA side; a ring of
  tables, one piece per frame in flight; the outputs on the device, one
  copy, sized for the largest frame the machine has (vertices, indices,
  modifier volume triangles, the records, the draw commands, the work
  buffer); the eleven kernels; the descriptor sets; two timestamp
  queries per frame. A device without host-cached memory, or whose
  limits are too small, keeps TA processing on the CPU.
- Each frame: the header pass writes the tables before the full parse;
  the decode is recorded into a command buffer of the texture pool's,
  submitted with the frame before its draws: clears of three small
  ranges, then the kernels with a barrier between each, the strip-
  counted ones dispatched indirectly from counts the GPU wrote. Nothing
  is read back; the GPU timestamps are read once their frame is known to
  be done, without waiting.
- The texture pool's fences are polled each frame to say how far the GPU
  has got, so that the TA side reuses a slice only after the work that
  read it. CommandPool gains IsDone(); an Allocation can say what kind
  of memory it is.
- Turned off while a game runs, the pool is detached and the buffers go
  once the frames that may read them are done (DeferDelete()).

Until the raster renderers draw from the GPU's buffers (the next stage),
every frame is still parsed on the CPU and drawn from that, so with the
option on the picture is the same and the decode only runs: the option
is for testing and measuring for now. Off, the renderer makes nothing
and pays one call a frame.
2026-10-15 13:48:46 +02:00