10 Commits
Author SHA1 Message Date
libretroadmin fab5ee678c Vulkan: the per-pixel renderer draws for the temporal upscaler too
The per-pixel (OIT) renderer renders at the size the frontend's
temporal upscaler asks for, under its jitter, keeps each frame's depth
and draws its motion vectors over it, as the per-triangle renderer
does. Its depth is one image a frame slot while the upscaler has the
frames, stored by the last render pass instead of dropped, with the
render passes' framebuffers made for it; SSAA is off then, as the
upscaler is the anti-aliasing and reads a single-sample depth.

The motion vector pass is one piece of code for both renderers
(rend/vulkan/motion_pass.cpp): the pass, its pipelines by cull mode,
the vectors' images and the strips' previous positions.

While the upscaler is active the Internal Resolution and SSAA core
options have no effect, and are hidden.

tools/vulkan/vk_hdr.sh and vk_sanitize.sh run the per-pixel renderer
with the upscaler, the inputs read back and checked under the
validation layer and the sanitizers.
2026-10-25 15:37:29 +01:00
LibretroAdmin 068183f906 vulkan: per-pixel fragment buffer sized from the screen, and a frame that overflows it is drawn in bands
The per-pixel renderer keeps every translucent fragment of a render pass
in one buffer, 16 bytes each. It was as large as the "Accumulation Pixel
Buffer Size" option said (512 MB by default), cut down to what the device
can bind as one storage buffer. A render pass with more fragments than
that dropped the ones past the end: the polygons drawn last lost them.

Measured on Jet Grind Radio's character select (charsel.state) at 8x
(5120x3840), with the count the shaders already keep, read back as below:
the frame asks for 12.1 million fragments, 0.62 a pixel. A 512 MB
buffer holds 33.5 million, so this frame does not overflow it on a device
that binds 512 MB. On one that binds 128 MB (Mesa's software Vulkan, many
mobile GPUs) it holds 8.4 million, and the frame loses its last-drawn
translucent sprites outright: the MEW / GUM / TAB names at the bottom
of the screen are missing.

- The buffer is sized when the renderer starts or the screen changes
  size, never while a game runs: two fragments a pixel of the screen
  (rend/oit_size.h), at least 32 MB, at most half of the video memory
  budget (rend/vram_budget.h) that was left. At 1x that is 32 MB; at 8x
  629 MB where the device can bind it.
- A device that cannot bind that much as one storage buffer (the lesser
  of maxStorageBufferRange and maxMemoryAllocationSize) gets it in up to
  four storage buffers of one size, binding 0 an array of four. The
  shaders find a fragment's buffer by comparing its index with the
  first's length: a branch every GPU takes the same way where there is
  one buffer.
- Every render pass's count (the shaders count the fragments that find
  no room too) is copied out after it ends, into host-visible memory for
  its frame slot. When the slot comes round again its fence has passed,
  and the CPU reads what that frame asked for. Nothing waits for it.
- A frame that asked for more than there is room for is followed by
  frames drawn in horizontal bands: each band is all of the frame's
  render passes, clipped to the band by the scissor, with lists that
  start empty and need the room of one band's fragments. The count goes
  up at once and down after a second of frames that would have fitted
  in fewer. Nothing is dropped and nothing is allocated.
- The picture's attachment is cleared by the first band and loaded by
  the others: a render pass variant (@resumed) differs from the usual
  one in that attachment's load op and initial layout only, so every
  pipeline works with both. The last band's final render pass covers
  the whole picture, as the frame's final render pass always has, for
  what is drawn after it (the VMU screens, the texture copy).

A frame with room for its fragments, which is every frame where the
buffer fits, is drawn exactly as before, plus a 4-byte copy for each
render pass. Three frames back is the earliest a count is seen, so the
first frame or two of a scene that overflows still lose fragments.

Tested on Mesa's software Vulkan at 8x with the buffer cut to 128 MB
(2 bands) and at 2x cut to 8 MB, without SSAA (2 bands) and with 4x
SSAA (15 bands): each picture is identical, to the pixel, to the same
frame drawn with room to spare. Without bands the 128 MB picture lost
the bottom names.
2026-10-15 18:52:56 +02:00
LibretroAdmin 24cb5e2e51 vulkan: SSAA in both raster renderers
A new core option, SSAA (reicast_ssaa): Off, 2x, 4x, 8x; Off by default.
Each pixel of the Internal Resolution is drawn from that many samples
and resolved by the shared rule (rend/tile/pvr_resolve.h). It is taken
up when the renderer starts and when the option changes, which is a
change of size (Resize()); nothing is made, compiled or read back while
a game runs for it.

How many samples (VulkanContext::SsaaSamples()): the most of those asked
for that the device can draw colour, depth and stencil with
(ssaa_device_samples()), and then that the video memory budget the
context read at start-up has room for (ssaa_clamp_samples()): the
screen's colour and depth, per sample, after the per-pixel renderer's
lists; nothing per sample for the per-triangle renderer where the device
has lazily allocated memory, since its sample attachments are transient. One line in the log says what was asked and
what was taken. It needs sample rate shading, which the core enables on
the device it makes (VkCreateDevice); a device the frontend made without
the core gets no SSAA, and says so.

The per-triangle renderer (drawer.cpp, pipeline.cpp):
- The screen's and the textures' render passes (MakeSsaaRenderPass())
  draw into colour and depth attachments of N samples, transient, cleared
  and never stored (load CLEAR, store DONT_CARE), and a second subpass
  resolves the colour into the picture (SsaaResolve, quad.cpp): a
  triangle over the framebuffer reads each sample through a
  subpassInputMS, as the unorm8 the attachment holds (packUnorm4x8()),
  and writes (sum + N/2) / N per channel. Never the device's resolve.
  The picture is all written by it, so it is not loaded.
- Pipelines get the sample count and a rate bit in their key (bit 30):
  shaded at each sample (sample shading, minSampleShading 1, the inputs
  "sample") or once a pixel. Once a pixel are the polygons drawn at a
  single rate (SsaaSingleRate(): sprites, the background, and flat strips
  where the decoder looks for them); their inputs are taken at the
  pixel's centre (interpolateAtOffset(v, 0)) where they cover all of a
  pixel's samples, which is where a frame without SSAA takes them (with
  more than one sample the device may take them elsewhere), and at the
  centroid where they cover only some, so that an edge pixel is not
  shaded from beyond the polygon. Both rates of a key are made together,
  so SSAA adds no compile of its own to the lazy creation that is there
  already.
- The last picture under a partial render, and modifier volumes, go into
  the samples; the VMUs and the OSD are drawn after the resolve.
- Renders to a texture are supersampled only where their sample
  attachments cost nothing (lazily allocated memory: tilers), with the
  attachments made at start-up as large as such a texture can be (1024 x
  the render-to-texture upscale a side). Elsewhere they are drawn with one
  sample, as without SSAA: at 4x that preallocation is 48 MB at 1024 x
  1024, more than the whole of what brute force (twice the Internal
  Resolution) adds, and what a game reads back of such a render stays what
  it is without SSAA.

The per-pixel renderer (oit/):
- Four subpasses with SSAA (RenderPasses::MakeSsaaRenderPass()): 0 depth
  and stencil, 1 colour and the translucent lists, 2 the translucent
  modifier volumes, 3 the picture put together and resolved, in one
  pass: the lists are read and sorted once a pixel, blended at each
  sample onto that sample's opaque colour, and the samples resolved by
  the shared rule. The sample attachments are one image each for all
  the render passes of a frame: a frame of one render pass never stores
  them; one that is not the last puts each sample together into them in
  subpass 2 (a per-sample variant of the same shader) and writes the
  translucent depth there, for the next render pass to draw over. They
  are not transient, since such a frame stores them.
- Textures are rendered to with one sample a pixel, as without SSAA,
  with shaders of their own (rttShaderManager): see the per-triangle
  renderer for why.
- A translucent fragment of a sorted pass is put in the lists once a
  pixel, with the samples it covers and is in front of the opaque depth
  at in eight bits of its polygon number's word (polygon numbers are
  under 2^22 then): the lists do not grow by N. Where triangles that
  share an edge cross a pixel each has a fragment there, with the
  samples that are its own; small triangles make a pixel's lists longer
  that way, and the most fragments sorted a pixel goes from 32 to 96
  with SSAA (without it, unchanged). Translucent modifier volumes count
  where they cover a pixel's first sample, so that two triangles of a
  volume sharing an edge count once.
- Stencil and depth are read as subpassInputMS at the sample a shader
  runs for (or the first one it covers, once a pixel).

Shaders without SSAA are built from the same text as before but for the
names of their inputs: a frame with SSAA off is drawn as it was (checked
against the build before, byte for byte, below).

GPU-side TA processing: the GPU's decode does not look for flat strips
(it would have to read every vertex on the CPU to choose a pipeline), so
with TA Processing = GPU only sprites and the background are drawn once
a pixel, and flat strips are shaded at each sample; ta_set_flat_strips()
is asked for only when frames are parsed on the CPU. The two give the
same picture with SSAA off, and pictures that differ by that with it on.

The log, for measuring: the GPU time of the screen's frames (two
timestamps a frame, read when the frame's slot of the command pool comes
round again, after its fence; never waited for) as mean, 99th
percentile and longest at the end (GpuFrameTimer), and the video memory
in use then (LogVramUsage()).

No change to the save state; run-ahead stays exact.

Tested on lavapipe (Mesa's software Vulkan, samples 1 and 4 only: 8x is
4x there, 2x off), RetroArch with the validation layer, over saved and
booted scenes (Jet Grind Radio, Sega Rally 2, Virtua Fighter 4, the live
test's disc), both renderers, threaded rendering on and off, TA
processing on the CPU and the GPU, run-ahead, Internal Resolution 2x:
- no validation error in any of them, SSAA on or off;
- SSAA off: the screenshots are the build before's, byte for byte;
- the live test's disc (2D: sprites and flat polygons, two render passes,
  two volumes, modifier volumes, a render to a texture, punch-through and
  sorted translucent polygons) with SSAA 4x: the build before's picture,
  byte for byte, in every one of the eight ways it was run;
- tools/threads/headless.sh, with and without reicast_ssaa=4x: the
  verdicts and the sound of the build before; ta_gpu_test and
  texdecode_test pass.

GPU time (the log above; lavapipe, so the figures only show the
direction), Jet Grind Radio, 175 frames, against brute force - SSAA off
at twice the Internal Resolution, the same samples a pixel:
- per-triangle: SSAA off 38.0 ms mean (p99 111.1), SSAA 4x 128.1
  (p99 174.9), brute force 111.0 (p99 154.6);
- per-pixel: off 110.9 (p99 155.7), 4x 472.1 (p99 575.0), brute force
  369.1 (p99 441.5).
A software rasteriser shades every sample of a pixel as it shades a
pixel, has no compressed samples and no faster MSAA path, so SSAA cannot
win there; the comparison that decides it is on the NVIDIA machines.
Video memory, worked out from the attachments made (lavapipe's heap is
the host's, shared): SSAA 4x at 640 x 480 adds 12 MB to the per-triangle
renderer (colour and depth of 4 samples, 14.7 MB, in the place of one
sample's depth) and 10 MB to the per-pixel one (in the place of its two
colour attachments and its depth); brute force adds 15 to 22 MB and 26
to 30 MB (every image at four times the pixels - the two or three
pictures of the frontend's ring, depth, and for the per-pixel renderer
its colour attachments and list heads), and the frontend's own scaling
on top.
2026-10-15 15:32:49 +02:00
LibretroAdmin fcffadcae2 vk: one frame is done with what it shares before the next uses it
The Vulkan renderers draw every frame into the same depth attachment,
and the per-pixel one into the same colour attachment, per-pixel
lists and fragment counter as well. Nothing told Vulkan that one
frame, or one render pass of a frame, had to be done with them before
the next cleared, loaded or reset them. Most drivers keep the order
on their own account. One that does not shows one frame's leavings in
the next: upstream fixed the per-pixel renderer for MoltenVK on macOS
15, which no longer does.

- per-pixel: upstream's dependencies into each subpass from outside
  the render pass, and between the subpasses for the colour
  attachment; barriers round the reset of the fragment counter. One
  more that upstream's layout does not need: the depth attachment is
  stored at the end of the last subpass and changes layout after
  that.
- per-triangle, screen and render to texture: the same for their
  depth attachment, which upstream's change does not reach.
- The clear of the per-pixel lists' image, added three patches ago,
  followed a barrier that made the image ready for shaders rather
  than for the clear. It says the right thing now.

Checked with the validation layer's synchronization checks, which
the live test can now turn on (next patch). Before: hazards in the
first pass of each renderer - the clear after its barrier, the colour
and depth attachments at the start of the render pass, the depth
attachment at its end, and the depth attachment between one frame's
submission and the next. After: none, in the Vulkan passes of the
live test that were run with it - all of both renderers' but the
ones that try the buffer orders, the fullscreen passes included.

Whether any driver in use showed anything for this is not known; the
software one the test runs on does not.
2026-10-08 05:54:51 +02:00
LibretroAdmin 8cae9ee35c vk oit: the per-pixel lists are empty when they are made, and the first frame is not red
"vk oit: the first frame after the per-pixel lists are made has its
translucent polygons" drew that frame like any other, after emptying
the lists with a full-screen draw in the colour subpass. The draw did
not empty them: in that first frame every pixel still had a list,
starting at whatever fragment was stored first, and the picture came
out covered by it - red all over on the test disc, whose first
translucent polygon is red. One frame, each time the renderer starts.

Before that patch the frame's last step was left out and its picture
was nothing in particular, so this is that patch's doing as far as
what shows goes: a frame of red in place of a frame of whatever was
in the image.

It shows for longer than a frame when the game renders only part of
the screen: the rest is kept from frame to frame, and stayed red. A
context destroyed and made again starts the renderer afresh, so the
picture kept across it (rend/last_picture.h) was put back and then
covered, on this renderer alone.

The image that holds each pixel's list is now cleared to "empty" when
it is made ready, with a clear command outside the render pass. The
draw in the colour subpass goes, with the pipeline that was made for
it and the render pass's dependency of that subpass on itself.

Found by the live test once its fullscreen toggles were made to fall
while the disc renders in part (the next patch): the first three
pictures after each restart were written out, the first was red, and
is right with this. The per-pixel Vulkan passes pass with the
validation layer on.
2026-10-08 05:01:22 +02:00
LibretroAdmin d0eac165fe vk oit: the first frame after the per-pixel lists are made has its translucent polygons
The per-pixel Vulkan renderer keeps a list of translucent fragments
for every pixel, and empties the lists at the end of each frame. In
the first frame after they are made - the renderer starting, a
change of size, a context made again - they hold nothing that means
anything, and that frame's translucent polygons were left out
altogether rather than put into them. A game that draws one frame
and then waits showed that frame without them for as long as it
waited. From upstream, which names the background of the progress
window in Fighting Force 2.

In that frame the lists are now emptied in the colour subpass, before
the first translucent polygon, and the frame is drawn like any other.
The emptying pipeline is made for that subpass as well as the last,
and the render pass lets the colour subpass depend on itself, which
the barrier after the emptying needs.

(That first frame also emptied the lists at its end with the wrong
vertex buffer bound - the full-screen quad's was only bound in the
part that was skipped.)

The live test passes on vulkan with the validation layer on, which
takes the per-pixel renderer through its first frame. Nothing checks
that frame's picture: not tested beyond that.
2026-10-07 19:36:13 +02:00
LibretroAdmin 500c2bb2a7 vulkan per-pixel: unsorted translucent polygons may write depth
Translucent polygons that the game says are already in order are not
put through the per-pixel lists. They are drawn in the colour subpass,
in the order sent, each tested against the depth the ones before it
wrote. The depth buffer is an input to the shaders in that subpass and
was in a read-only layout, in which Vulkan does not allow depth
writes:

  vkCmdDrawIndexed(): depthWriteEnable is VK_TRUE, while the layout
  (VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL) of the depth aspect
  of the depth/stencil attachment in the render pass is read only.

This is the case "no depth writes in the colour pass where there is
nothing to write" left alone, because here there is something to
write.

A frame that has such polygons is now drawn with render passes whose
colour subpass has the depth buffer in the general layout, which
allows reading it as an input and writing it. They differ from the
usual ones in that layout and nothing else, so the same pipelines and
framebuffers serve both. Frames without unsorted translucent polygons,
which is every frame of most games, use the read-only layout as
before.

With the next patch's disc, whose second render pass is pre-sorted,
the validation layer reported the error above for every such draw and
now reports nothing.
2026-10-06 07:25:50 +02:00
LibretroAdmin 4bd929c953 threaded rendering: make one frame per retro_run, the one non-threaded makes
With Threaded Rendering on, nothing tied the emulation to retro_run. The
emulation thread ran free, held back only by the audio it pushed to the
frontend itself, 512 samples at a time, from its own thread, whenever
that call blocked. retro_run waited up to 100 ms for whatever render
turned up and returned a duplicate frame if none did. A render the game
started while the last one was still being drawn was thrown away. So a
retro_run could last any length, carry a new frame or not, and its audio
arrived some other time from some other thread.

Each retro_run now makes exactly one frame, the same one it makes with
Threaded Rendering off:

  - retro_run wakes the emulation thread, which runs the machine up to
    vblank and goes back to sleep, as dc_run() does in retro_run itself
    when rendering is not threaded;
  - when the game starts a render the emulation thread hands it to the
    libretro thread and sleeps; that thread reads what the frame takes
    from the game's memory (display lists, textures, framebuffer), lets
    the emulation thread go on, and draws. A render to a texture, which
    ends up back in the game's memory, keeps it asleep until it is done;
  - every render is drawn. The emulation thread waits for the one before
    to be finished; none is dropped;
  - at vblank the frame's audio is sent from the libretro thread in one
    batch, together with its video.

The renderers read some PVR registers while they draw, by which time the
emulation has moved on and may have changed them. Each render now
carries a copy of the registers made when the game started it, and the
renderers' files read through that (PVR_REGS_FOR_RENDERER), so they draw
from what non-threaded rendering would have read.

EmuBaton (core/libretro/emu_baton.h) is that handshake. There is no
lock, no sleep and no timeout in it: every change of hands is one atomic
operation and a thread with nothing to do sleeps on an eventcount. A
save state from the frontend's task thread takes the machine between
two frames.

The "synchronous rendering" and "delay frame swapping" options have
nothing left to choose and are hidden.

tools/threads/live.sh runs the test disc for the same 300 frames with
Threaded Rendering on and off and requires the same screenshot, byte
for byte, and the same save state (live_same.py leaves out the host
addresses a state carries). The old threaded path fails that: its state
differs from the non-threaded one. tools/threads/run.sh puts the new
EmuBaton under ThreadSanitizer.
2026-10-05 13:39:02 +02:00
Flyinghead bc61520174 vulkan: delay destroying in-flight images
Keep in-use imave and image views when the texture is updated.
Wait until the graphics queue is idle before changing shared buffers and
attachments.
Refactoring
2019-12-09 21:16:16 +01:00
Flyinghead dce6d1c0ba vulkan: oit renderer 2019-12-03 18:33:55 +01:00