63 Commits
Author SHA1 Message Date
libretroadmin 195f56511a Renders to textures: the screen's attachments, and only their own pixels
A render to a texture draws with the screen's attachments - the depth
with the per-triangle and per-strip renderers, the colour and depth the
per-pixel renderer keeps between passes - made at least as large as a
render of a screen, 640x480 times Render To Texture Upscaling
(TextureDrawer::ShareExtent()). It draws its own pixels, not the whole
power-of-two texture they are in; the rest is cleared black once, when
the image is new or was written since (Texture::ClearOutside()).

Nothing is made for a render during play, and the video memory is the
screen's: no more than before renders to textures had attachments made
up front, and up to 16 MB less than with them, at any resolution where
the screen is at least as large as the renders. A render larger than the
screen has attachments of its own made when it comes, logged. With SSAA
or the upscaler the texture drawers keep their own.

tools/vulkan/vk_vram.sh: the test disc built with -DLIVE_ODD, a render
of no power of two drawn as its whole texture, each raster renderer at
1x, 2x and with 2x renders: every render drew with the screen's
attachments, black past it (tools/rend/odd_check.py), validation clean,
the image memory at the most printed (vk_hdr VKH_IMAGE_BYTES). In CI.
2026-11-10 06:25:56 +01:00
libretroadmin 53bc61f4d8 Renders to textures: attachments and read-back room made before play
The texture drawers' attachments are made with them, as large as a
render to a texture can be (TextureDrawer::MaxSide()), as SSAA's already
were: the per-triangle drawer's depth, and the per-pixel drawer's colour
and depth with their framebuffers (OITDrawer::MakeAttachments(), split
from MakeBuffers()), each with a framebuffer's place for every image. The
room a render read back into video memory is put in is reserved before a
renderer's first frame for one of the screen's size (rtt_watch_reserve()).
A game's first render to a texture no longer makes them mid-game.

Four sites come off the list of allocations during play, 22 left.
2026-11-10 03:57:13 +01:00
libretroadmin bc87a9a59d Vulkan, OpenGL: modifier volumes and sorted indices from where they are parsed
The parse's arena has a part for each list the card draws from: the
vertices, the indices (with the sorted passes' room after them), and the
modifier volumes' triangles and those cut (rend/modvol_clip.h); a frame's
lists are moved into a slice together (parse_arena_attach()). The volumes'
triangles a frame draws are drawn from there with Vulkan and OpenGL, not
copied into the main buffer or uploaded each frame. With OpenGL the
per-triangle sorted passes' indices are written after the frame's, in its
index buffer, not uploaded to a buffer of their own; the per-pixel
renderer's volume array is set again when its buffer changes.

The heavy disc has a shadow volume a billion pixels off the screen,
first in its list, over a polygon that takes shadows: the volumes' cut
triangles, and those after it, drawn from their part (vk_parse_arena.sh,
gl_parse_arena.sh), which also take the disc with volumes of each cull
mode now.
2026-11-02 16:01:46 +01:00
libretroadmin b48265ebd6 TA parse: C89 lists and strip indices; Vulkan draws from where it parses
The frame's lists are ta_list.c's (C89): List<T> is a typed facade over
it. A strip's indices are ta_index.c's C89 kernel: one branch-free check
of the whole strip, then a straight run of indices; the per-vertex loop
only where a vertex is bad or the room runs out. The parse is about 20%
faster (2.74 to 2.17 ns a vertex on the heavy disc), its output the same
byte for byte.

With Vulkan on a machine whose video memory the CPU reads back fast (one
memory, as lavapipe's and integrated GPUs'), the vertices and indices
are parsed straight into a slice of memory the card reads, one slice a
frame in flight (rend/parse_arena.h, vulkan/parse_arena_vk.h), and the
translucent sort writes after them: no copy into the main buffer. Other
machines copy as ever.

Tests: tools/pvr/ta_index_test.sh (against the old loop, 1M strips),
tools/rend/parse_arena_test.sh, tools/vulkan/vk_parse_arena.sh (every
render in place, pictures those of the copy, each renderer, threaded,
upscaler, a change of size), tools/pvr/ta_parse_bench.sh; vk_hdr gains
VKH_UNCACHED, VKH_OPTS_AT and VKH_BUFFERS.
2026-11-02 13:11:59 +01:00
LibretroAdmin 6df4ce02e8 Vulkan per-pixel renderer: a frame slot's count goes to the bands of its kind
What a frame slot counted is read by whichever drawer comes to the slot next
and goes to the bands of the kind of frame drawn there, the screen's or a
render to a texture's, kept apart in OITBuffers (rend/oit_bands.h). The two
take turns at the slots, so a screen whose translucent fragments overflow the
buffer is drawn in bands with any number of renders to a texture a frame and
any count of slots, as the other per-pixel renderers do.

tools/rend/oit_bands_test.sh checks it over every count of slots and of
renders to a texture a frame.
2026-11-01 19:33:18 +01:00
libretroadmin 2c40409532 Vulkan: what is the same from one frame to the next is not made again
A polygon's texture descriptor set is written once and kept, a frame slot's
for as long as its frames bind it, known by the serials of the views it
holds (a view's serial is never another's, where a handle can be made
again at the same value), and handed to the draw that binds it with no
second lookup: plain arrays, an open-addressed table and a stack of free
sets. A render to a texture keeps its framebuffer while its attachments
are the same ones, the command pools keep the memory their commands were
recorded in and submit from a list kept for it, and the descriptor writes
and the main buffer's pieces of a frame are arrays on the stack.

The temporal upscaler writes where each vertex was straight into the
memory the GPU reads it from (the Vulkan buffer mapped, Direct3D 12's
upload ring), and matches strips through grow_buf.h memory and tables kept
from frame to frame - the kept strips found by signature through a
counting sort's index - instead of vectors and maps made again each frame.
2026-11-01 16:34:05 +01:00
LibretroAdmin 8245523a55 Texture cache: make again only the parts of a texture that changed
A texture watched by its hashes is hashed by 4 KB pieces of video memory.
When only some of them changed, and nothing else (palette, stride), the
regions of the image those pieces are in are made again and the rest is
left: twiddled, planar, compressed (unless the codebook changed), paletted
and mipmapped textures down to their 8x8 level, up to 16 regions, while
they cover less than half of it. Vulkan makes them on the GPU over those
groups only (vkCmdDispatchBase) from just their bytes, or copies the rows
of a planar one; converted on the CPU, Vulkan, Direct3D 11 and 12 and
OpenGL upload just them. Direct3D's and OpenGL's decoders make the texture
whole; so do textures scaled up, dumped or replaced.

tools/rend/texconv_test.sh part holds every update of textures of each
kind, written a few bytes at a time, to what a full upload makes, on the
CPU and through the kernel's text.
2026-10-28 16:41:20 +01:00
LibretroAdmin 0c4f0dcc46 Vulkan: set the modifier volumes' and prepass's cull mode with the draws
Where the device has VK_EXT_extended_dynamic_state, the modifier volume
and depth prepass pipelines are made once for the three cull modes, with
the cull mode dynamic, and a block of their draws sets it when it
changes. Without it they are made for each cull mode as before; the
choice is made once when the renderer starts.
2026-10-28 11:06:39 +01:00
LibretroAdmin c41cfa30dc Vulkan: write the translucent polygon parameters straight into the buffer
The per-pixel renderer writes each translucent polygon's two words where the shaders read them, instead of into a vector made each render and copied after.
2026-10-28 08:23:04 +01:00
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 e7cde6e176 HDR: ACES 2.0's tables off the frame; SSAA and the X scaler exact
ACES 2.0's tables (the JMh parameters, the tone scale's LUT and the gamut
scales) were made on the frame that changed the peak, the gamut or the
surround. They are now made by the HDR worker, like the cube, through
the same lock-free hand-over: a slot for each kind of job taken by
atomic exchange, a CAS-pushed list of what is done, a stop flag for each
job, the worker parked on an eventcount. Nothing waits for it. Until the
new tables are done the encode keeps the old ones, and their cube, with
the gamut they were made for; before any, the tone scale alone
(HDR_DT_ACES2). A fill takes back what the worker finished before it
reads the tables, so a block is never half of the old tables and half of
the new ones. ACES2_InitSurround() is the reentrant init the worker
calls. hdr_dt_finish() builds in place, for the tests. Without threads
(TARGET_NO_THREADS) the build is done in place as before.

With the write-out off, what SSAA resolved is now encoded from the exact
average of its samples, not from the byte the picture rounded it to, and
is not dithered. Where something was drawn over the picture after the
resolve (the VMUs, the crosshairs), the picture is encoded as before:
the encode checks that the picture is the samples' resolve by the shared
rule (pvr_resolve_join()).
 - Vulkan per-triangle and per-strip: the HDR subpass reads the samples
   as a second input attachment.
 - Vulkan per-pixel: the last pass also stores each pixel's channel sums
   (OIT_FINAL_FRAG_S1_SUMS, an R16G16B16A16_UINT image in the OIT
   buffers' set), which the encode samples.
 - Vulkan compute: the walk stores the sums too, under a specialisation
   constant, which is 0 at 24-bit. It does the same for the X scaler's
   pairs of pixels, at one sample a pixel as well. Renders to a texture
   have walks of their own that store none.
 - Direct3D 12 per-triangle: the encode reads the multisampled target
   (PS_SSAA, hdr_encode_dxbc.inc).
The sums are scaled to 16 samples, so one encode pipeline takes any
count. Direct3D 12's compute kernels are unchanged: kernels_dx12.sh
leaves the PVRC_SUMS blocks out of their source, so their DXIL and sums
are as they were. At 24-bit none of this runs.

tools/vulkan/vk_hdr.sh gains --exact cases on two new discs built from
live_prog.c (live_disc.py --slant and --hscale). The first has a slanted
shaded triangle and is run on every renderer at 4x. The second adds the
X scaler into a 32-bit framebuffer and runs the compute renderer at one
sample a pixel. The old code fails both: it dithers, at most 1.08 of a
step off. tools/hdr/hdr_d3d.sh 12ssaa checks Direct3D 12's encode with
samples, with an overdrawn strip. tools/hdr/hdr_cube.sh checks the
tables: the first fill and a gamut change keep what there is, and both
converge on the tables made in place, and no block between is
a mix of two sets of tables. vk_sanitize.sh adds the per-pixel
and compute renderers with SSAA at scRGB.
2026-10-25 00:13:22 +02:00
LibretroAdmin 70304fcd02 vulkan: the chip's write-out before the VMUs and crosshairs, as Direct3D and OpenGL have it
On Vulkan the VMU screens and lightgun crosshairs are drawn in the
frame's render pass (its last subpass), so the write-out pass, recorded
after the render pass, filtered and dithered them too. On the console
they are not part of the picture at all; Direct3D and OpenGL already
draw them after the pass.

Now, when the frame has a write-out and there is something to draw over
it, the screen drawer ends the frame's render pass, records the pass,
and the VMUs and crosshairs go in a render pass of their own that
loads the picture (VulkanWriteOut::BeginOverlay()), with a quad pipeline
made for it. The render pass, its framebuffers (one per picture) and
the pipeline are made with the pictures, when the renderer starts or
changes size. Nothing is copied, and nothing is made in a frame. A frame
without a write-out, or with nothing over the picture, records what it
did before. With the option off, nothing changes.

Gates (lavapipe; not a real GPU): RetroArch, VF3tb booted to frame 900
with VMU screen 1 shown (opaque), for the per-triangle and per-pixel
renderers:
- option off: the same picture as before, byte for byte;
- option on: outside the VMU screen (and the line under it, whose
  filter taps reach it), the option-off picture through the model of the
  filter and dither; on the VMU screen, the VMU as drawn, unfiltered.
  No validation errors.
2026-10-24 22:13:48 +02:00
LibretroAdmin ca3fee8eaa rend: the chip's write-out on the raster renderers, with the 16-bit dither; the old PowerVR2 filter goes
The core option is now "PowerVR2 scaler, flicker filter and dither"
(reicast_pvr2_scaler, still off by default). It does what the
PowerVR2 does to the picture as it writes the tiles to video memory,
in the chip's order: the X scaler, the Y filter, then the packing to a
framebuffer of 16 bits a pixel and its dither (FB_W_CTRL). The picture
shown is that framebuffer as the video output reads it back, with
FB_R_CTRL's fb_concat in the low bits.

- rend/tile/pvr_scaler.h: pvr_fb_word() and pvr_fb_write(), the
  packing to 0555/1555 or 565 with rend/rtt_watch.cpp's 4x4 pattern
  (the same arithmetic as the renders to a texture). At internal
  resolution N the pattern goes by the chip's pixel (N picture pixels
  to one). 4444 and the 24/32-bit formats are left as they are.
  Checked against an independent copy of rtt_watch.cpp's packing plus
  TexCache.cpp's read-back: no difference in 195072 cases.
- The compute renderer's in-place pass (PVRC_K_YFILTER) does the
  packing and dither after the Y filter, and runs on any frame with
  either (ComputeFrame::WriteOut()). tile_ref and refsw do the same.
  refsw did not dither the picture before: it does now.
- The raster renderers run the same pass in place over their picture
  after the frame (before the VMUs and crosshairs on Direct3D and
  OpenGL; on Vulkan they are in the frame's render pass, so the pass
  runs over them too):
  - Vulkan, both renderers: the compute renderer's own SPIR-V
    (vulkan/writeout.cpp).
  - Direct3D 12, both renderers: the compute renderer's own DXIL
    (dx12/dx12_writeout.cpp). Needs typed UAV loads of R8G8B8A8_UNORM.
  - Direct3D 11, both renderers: the same pass in HLSL, compiled when
    the renderer starts (dx11/dx11_writeout.cpp). Needs typed UAV
    loads of R8G8B8A8_UNORM (TypedUAVLoadAdditionalFormats).
  - OpenGL 4.3 and later, both renderers: the same pass in GLSL,
    compiled (and cached) when the renderer starts
    (gles/gl_writeout.cpp). The frontend's framebuffer is not a
    texture of the core's, so the picture is blitted to the pass's
    texture and back.
  Not on OpenGL ES, OpenGL 3 or Direct3D 9.
  The pictures are made writable by a compute shader only when the
  option is on. Turning the option on or off counts as a change of size:
  the renderer makes its pictures again between frames, never during
  one. Off, the renderers do what they did before, at no cost.
- The rasterizers' X scaler is not done here: they still squash the
  game's double-width drawing in the transform, as before. No dumped
  game sets SCALER_CTL's horizontal scaling.

The old "PowerVR2 Post-processing Filter" (reicast_pvr2_filtering,
OpenGL only) is removed: PostProcessor, gles/postprocess.cpp/.h, the
PowerVR2Filter setting, the option in every language, and its
Makefile entry. It guessed at a 3-tap vertical blur, which the exact Y
filter now replaces. It also dithered by its own rules and added a made-up
"VGA signal loss" smear, which the hardware does not do. It compiled its
shaders mid-game and drew through an extra framebuffer.
RetroArch ignores old values of the option left in users' configs.

Gates (lavapipe/llvmpipe and vkd3d, not a real GPU):
- tile_test VF3tb 1x on Vulkan, at 1 and 4 samples, and on Direct3D 12,
  at 1 and 4 samples: off, filter + 565 dither (VF3tb's own
  registers), 555 dither alone, and taps 3 lines apart with fb_concat 5.
  16/16 runs are the same as tile_ref on each.
- RetroArch (Vulkan, then glcore) with VF3tb booted, frame 900, for each
  of Vulkan per-triangle, Vulkan per-pixel, OpenGL per-triangle and
  OpenGL 4 per-pixel. With the option off, the picture is the same as
  master's, byte for byte. With it on, the picture is exactly the
  option-off picture through an independent model of the filter and
  dither (Y_COEFF 0x8040, 565, dither on). No validation errors.
- Direct3D 11 and 12 rasterizers: syntax-checked with mingw, and the
  Direct3D 11 pass compiled with d3dcompiler_47 (cs_5_0). Not run.
2026-10-24 22:13:36 +02:00
LibretroAdmin ae601c3aaa rend: a modifier volume triangle with a vertex far off the screen is cut before the GPU clips it
On the user's Jet Grind Radio frame (the jgr13 state) the per-pixel and
per-triangle renderers drew the street's shadow with a dark band down the
right of the picture, 51 pixels wide at every resolution (x from 589 of
640 at 1x, from 4709 of 5120 at 8x), and Direct3D 12 a strip along the
top as well - in Vulkan and in Direct3D 12 (vkd3d-proton), on lavapipe,
on master. refsw and the compute renderer, which take the exact
geometry, draw neither (tools/rend/tile_geo.c holds the compute
renderer to it; refsw is held to it since its far-out triangles are set
up exactly).

The shadow volumes have vertices 1e9 pixels out, at 1/w 1e5 (w 1e-5),
and the raster renderers give them to the GPU as they are: a clip-space
vertex of (30, 30, ., 1e-5). The GPU clips such a triangle to the screen
in 32-bit float, and where an edge between two of those vertices crosses
the screen's side, the point it finds comes from coordinates of 30 that
cancel to 1e-5: its place on the screen is out by a fifth of the
screen's width. The cut polygon's side that lies on the screen's right
edge came out at x 589, so the volume triangle missed the strip to its
right, the strip was outside one face too few, and it was shadowed.
(A float32 model of the clip of the frame's volume triangles puts their
sides that far off; the band's edge is straight down at x 589 in every
row, the clip plane's side, not the line of any edge of the volumes.)

Now, while a raster renderer is drawing (ta_set_modvol_clip(), set in
each one's Process), a frame whose volume triangles have a vertex more
than 32768 pixels from the screen's origin has all its volume triangles
made again for the raster renderers (rend/modvol_clip.h, into
rend_context's mv_clip and mv_clip_range): each far one cut, in double
precision, to the square of that size about the origin and given as the
fan of up to five triangles the cut polygon makes; 1/w, which is linear
across the screen, taken along each edge. Every point of the fan is on
the triangle's edge to well under 1/256 pixel, the fan's triangles keep
its winding and share their edges exactly, and an edge two triangles
share is cut to the same points in both (from its ends in one order), so
a sample is inside one face of a volume or another as before, never in
two and never in neither. The GPU then has vertices no farther out than
the square, which its float clipping keeps exact. A volume left with no
triangle keeps one with no area, so its closing draw is still made.

It is made on both of the TA's ways: from the parse's triangles, and on
GPU-side TA processing from the frame's blocks (as the shadows' bounds
are, ta_gpu_shadow_areas()), so the raster renderers then draw the
volumes from the CPU's triangles instead of the decode's. A frame with
no volume triangle that far out - most games, most frames - is drawn as
before, to the bit: nothing is made, uploaded or drawn differently. The
compute renderer, refsw and the tools take modtrig as before.

The renderers draw each volume from its range in mv_clip when the frame
has it (mv_clip_range_of()): Vulkan (drawer.cpp, oit_drawer.cpp), OpenGL
(gles/gldraw.cpp, gl4/gldraw.cpp, gl4/abuffer.cpp), Direct3D 9, 11 and 12
(their renderers and per-pixel renderers). No shader changes; no new
allocation when running: the lists are made with the TA context, room
for half as many triangles again as modtrig, and a frame that would need
more is drawn as before.

tools/pvr/ta_gpu_test: the cut triangles made from the blocks are held to
the parse's, byte for byte.

On jgr13 with the user's options (SSAA 2x, which lavapipe draws as 1,
TA processing on the GPU, threaded rendering): the band is gone from
Vulkan's per-pixel and per-triangle pictures at 1x and 8x, from Direct3D
12's (per-pixel and per-triangle at 1x, per-pixel at 8x), and from
OpenGL 4's per-pixel and the per-triangle OpenGL one at 1x; Direct3D 12's
per-pixel picture is now within 8 levels of Vulkan's at every pixel, and
Vulkan's within 2942 pixels (more than 32 apart: texture filtering) of
the compute renderer's, from 25405. TA processing on the CPU draws the
same picture as on the GPU. Virtua Fighter 3tb and Jet Grind Radio's
select screen (no volume that far out) draw the same pictures to the bit.
Direct3D 11 and 9 are built and their volume draws changed the same way,
but not drawn here.

Gates: tools/rend/speckle_test.sh with jgr13 and the select screen state
(sources and device runs) PASS; tools/pvr/ta_gpu_test.sh (Vulkan, OpenGL,
jgr13 and s10: the cut triangles from the blocks the parse's, 2 frames
with some) and its Direct3D 11 and 12 runs PASS; tools/rend/tile_test.sh
with tile_geo PASS; texdecode_test PASS; tools/determinism deterministic;
tools/threads/headless.sh passed; kernels_spv, kernels_dx12 and
kernels_dxbc check (no shader changed).

Verified on lavapipe (Vulkan), llvmpipe (OpenGL) and vkd3d-proton on
lavapipe (Direct3D 12) only; unverified on a GPU, where the clipper may
differ (the band may be narrower, wider or not there at all on the user's
RTX 5090), and the cut geometry is the exact one whatever it does.
2026-10-24 11:36:58 +02:00
LibretroAdmin 28881bb0ad vulkan, gl4: a translucent modifier volume's closing draw is done before the next volume's draws start
The specks over the graffiti on Jet Grind Radio's select screen (per-pixel
sorting, reported on an RTX 5090, with the user's state) are the graffiti
shadowed where it is not: each speck is the graffiti's own colour,
dimmed, and they sit where translucent modifier volumes (the shadows)
overlap on the floor; the test below draws the same dimmed patches there.

The per-pixel renderers mark the translucent fragments a volume shadows
from the volumes' shaders: the volume's draws toggle a stencil bit in each
fragment behind one of its faces, then its closing draw (inclusion or
exclusion) reads that bit, clears it and sets the fragment's shadow bit.
Only a barrier orders what the shaders of two draws do to the same
fragment. The Vulkan renderer had one between a volume's toggles and its
closing draw, but none after the closing draw: on a GPU, which runs the
draws of a subpass at once, the closing draw of one volume ran while the
next volume's toggles were half done, cleared the bit they had just set
and marked fragments from a count that was not finished - a fragment
outside the next volume, between its first face and its second, came out
shadowed. A software device runs the draws one after the other, so it was
never seen here. Upstream found this in the same game in 2021 (the
barrier after the closing draw) and the fork never had it.

The OpenGL 4 renderer had the same two places, with
GL_BUFFER_UPDATE_BARRIER_BIT, which orders buffer updates from the API
(glBufferSubData and the like) and not what shaders write to a shader
storage buffer: neither the toggles before a closing draw nor the closing
draw before the next volume were ordered, nor the lists before the
volumes and the volumes before the pass that puts the lists together.

Now:
- Vulkan (oit_drawer.cpp): a fragment shader to fragment shader barrier
  after each closing draw too, as before it (the render pass's subpass
  dependency on itself allows it).
- OpenGL 4 (abuffer.cpp): glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT)
  before and after each closing draw, and the storage bit added to the
  barriers before the volumes and before the lists are put together. The
  barrier that was before every volume draw, which ordered nothing that
  matters there, is gone.
- Direct3D 12 had the UAV barrier on both sides already, and Direct3D 11
  orders a draw's UAV writes before the next draw's itself.

The cost is one barrier for each closed translucent modifier volume of a
frame (a by-region one in Vulkan), which a GPU needs to draw them right;
nothing else changes, and nothing is allocated, compiled or waited for in
a frame. The pictures lavapipe and llvmpipe draw are the same as before,
byte for byte (they ran the draws in order already).

tools/rend/speckle_test.sh, with the user's state: PASS - the sources,
and every Vulkan case (SSAA off, 4x, and 2x and 8x as the 1 and 4 samples
lavapipe has; TA processing on the CPU and the GPU; threaded rendering on
and off; 1x and 2x) and both OpenGL 4 cases draw the same picture with
the list draws reordered as a GPU may run them. On the commit before, the
Vulkan cases all drew another picture.

Verified here on software devices only. Unverified on an NVIDIA GPU: the
user's RTX 5090 is where it shows.
2026-10-24 07:32:38 +02:00
LibretroAdmin e585663b06 tools/rend: speckle_test, the specks a GPU draws and a software device never does
The specks on Jet Grind Radio's select screen came back with the raster
renderers on an RTX 5090, and nothing here could see them: lavapipe and
llvmpipe round every pipeline alike and run the draws of a subpass one
after the other, which a GPU does not. tools/rend/speckle_test.sh checks
for both ways specks have come about:

1. Depth computed differently by passes that test it against each other
   (what b78383f9 and the commits before it fixed). In the sources, with
   no device: no gl_FragDepth, SV_Depth or DEPTH output in any raster
   renderer (Vulkan, OpenGL/GLES, Direct3D 9, 11, 12); every Vulkan
   vertex shader whose position is depth tested places it with
   pvr_position() (position.glsl, invariant); the OpenGL ones declare
   gl_Position invariant; the Direct3D 12 vertex shaders place it with
   the precise transformVertex(), the Direct3D 11 positions are precise.
   On the sources before those commits (4fcbf561^) it fails 15 ways.

2. The per-pixel renderers' translucent modifier volumes change the
   fragments in the lists from their shaders: one draw toggles a stencil
   bit, the draw that closes a volume reads and clears it, and only a
   barrier orders what two draws' shaders do to the same fragment.
   - In the sources: each per-pixel renderer's closing draw has a barrier
     before it and after it (Vulkan, OpenGL 4, Direct3D 12; Direct3D 11
     orders a draw's UAV writes before the next draw's itself).
   - On a device, given a content and a state: the core is linked again
     with vulkan/oit/oit_drawer.cpp and gl4/abuffer.cpp built with
     RACE_TEST, under which the list draws between two barriers are
     recorded a triangle at a time, the draws' triangles taken in turn,
     the last draw's first - one draw's shaders running while another's
     are half done, as a GPU may run them - and each picture has to be
     the core's own, byte for byte. Vulkan on lavapipe with per-pixel
     sorting, SSAA off, 2x, 4x and 8x (lavapipe draws 1 or 4 samples, and
     has VK_EXT_post_depth_coverage, so the exact path), TA processing on
     the CPU and the GPU, threaded rendering on and off, 1x and 2x; OpenGL
     4 on llvmpipe. tools/lag/lagmeter.c is the frontend: a state loaded,
     the frame after six written out, the same every run.

The list draws go through a small ListDraws in each of the three
renderers (Direct3D 12's too, for its build with RACE_TEST); in a release
build it records each draw as before, with the same barriers: nothing
changes but where the calls are made.

On this commit the test FAILS, as it should: the Vulkan and OpenGL 4
sources have no barrier after the closing draw, and every Vulkan case
with the user's state of the select screen draws another picture with
the draws reordered - the graffiti on the floor shadowed where it is not,
next to the pole and right of the skater, where the specks are in the
user's screenshot. The next commit is the fix.

  sh tools/rend/speckle_test.sh                  the sources (a second)
  sh tools/rend/speckle_test.sh CONTENT STATE    and the device runs (about 4 minutes here)
2026-10-24 07:32:34 +02:00
LibretroAdmin eb19604d57 vulkan: the compute renderer draws renders to a texture itself
Every render to a texture was left to the raster renderer when Rendering
Method is Compute. Now the kernels draw it, with the same frame words as
a screen frame, into the very texture the per-pixel renderer would have
drawn it in:

- The texture is OITTextureDrawer's: the frame from its first pixel to
  the pixel clip's last, in powers of two, times Render To Texture
  Upscaling, taken from the texture cache the same way (the code that
  takes it is now one function both use, ComputeBegin() and NewFrame()).
  After the render it is locked in video memory and watched the same way
  (rtt_read.h), so the texture cache, the copy to video memory when the
  game first touches it, and what a later CPU write over the render does
  (the texture is made again from video memory, as with the raster
  renderer) are unchanged. Nothing is read back otherwise.
- It is drawn at one sample a pixel, as the per-pixel renderer draws
  renders to a texture whatever SSAA is. With SSAA on, the kernels for one
  sample are made at Init() as well; nothing is compiled while a game runs.
- The frame's words for such a render: only the pixel clip is drawn
  (times the upscaling, as the raster renderer's scissor), the rest of
  the texture is the clear colour as the raster renderer's render pass
  leaves it, and the transform is the raster renderer's viewport's, that
  many times larger.
- The bins are made at Init() for the largest texture rendered to there
  can be (1024 x 1024 at 8x), a few words a coarse tile; each frame clears
  only the part its own tiles use, so a screen frame clears what it did.
- The images rendered to are made with storage usage as well when the
  compute renderer is on (the pool's, TexturePool::SetTargetStorage(), and
  those the drawer makes outside it), so the kernels write them in place:
  no copy. When the device cannot write that format from a compute shader
  such renders still go to the raster renderer, counted under that reason.
- The texture leaves the walk in the layout textures are sampled in, made
  ready for fragment and compute shaders and for the copy to video memory.

The log says how many of the frames drawn were renders to a texture.

tools/rend/tile_dump.cpp writes renders to a texture too (rtt_NNNNN.bin,
TILE_DUMP_RTT, TILE_DUMP_RTT_EVERY), packed as the renderer packs them at
the size of their texture, and tools/rend/tile_test.sh draws and compares
them with the reference like the screen's frames.
2026-10-20 20:18:47 +02: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 23c3cce66f vulkan: the compute renderer, drawn by the per-pixel renderer
With rend_compute_wanted, the Vulkan per-pixel renderer draws the
screen's frames with the compute renderer (rend/tile/pvr_compute.h):
ComputeFrame packs the parse's frame into a ring slot of host memory,
ComputeDrawer records setup, binning, the walk and the clear into the
frame's command buffer, writing the picture the per-pixel renderer would
draw, which is presented the same way. Buffers and pipelines are made at
init from the frame size and samples; nothing is read back.

The frames it does not draw yet are drawn by the rasterizer as before:
renders to textures, GPU-side TA processing's, the framebuffer's, the
VMU and crosshair OSD's, those starting from a picture kept from another
context, those with a triangle behind the eye (which the rasterizer
clips and the kernels would leave out), and those with more textures
than the kernels' array. The flat shading's colours are moved onto the
first vertex only once nothing can leave the frame to the rasterizer,
which moves them itself.
2026-10-16 02:01:33 +02:00
LibretroAdmin 7791baf886 rend: the texture pool's rectangles and renders; Vulkan's upload ring made once
The texture pool (rend/tex_pool.h) made square shapes only at the start;
a rectangle had its image made the first time a game used one. A quarter
of the pool's share of the budget now goes to rectangles of every shape
from 8 to 1024 texels a side, by fixed shares: the smaller and the nearer
to square, the more (64x32, 128x64, 256x128 a few each; strips and the
large ones made when they are first used, as before, and counted).

Pictures rendered to are a kind of format of their own (TARGETS): a
fixed few of the shapes renders come in - 64x64 to 512x512 two of each,
1024x1024, four 1024x512 (a 640x480 render), two 512x256 and one of each
other two-to-one shape from 64 - half as many on a card with little
memory. The Vulkan renderers render to one of those
(Texture::CreateRenderTarget(), from both drawers), RGBA8 with the usage
a render needs; the Direct3D 11 ones take the spare a render is drawn
into from them (DX11Texture::TakeSpare()), with its render target view
made at the start, and give it back when the texture is done with it.
A render at a resolution above 1x is larger than any of them and is made
as before.

The Vulkan renderers' upload ring, where textures converted on the CPU go
to the GPU through, made 4 MB buffers when a frame needed more, and gave
them back after some ten seconds unused. It is one buffer now, made when
the renderer starts, a share of the budget (16 to 64 MB), handed out a
piece at a time, the frames' pieces one after the other. A frame's pieces
are free again when its place comes round (the texture command pool has
waited for its fence then); when the ring is full, the oldest frame
still out is polled (CommandPool::IsDone()) and its pieces taken back if
it is done. Nothing waits: with no room, the texture is made with the
next frame instead - marked dirty, its image (made one that can be
sampled, if it was new) left as it is until then.
2026-10-16 01:59:35 +02:00
LibretroAdmin 0102585127 vulkan: pipelines made at start and on a thread, never in a frame
Both raster renderers made a polygon's pipeline the first time a polygon
of its kind was drawn, inside retro_run: 15 (per-triangle) and 19
(per-pixel) of them in Jet Grind Radio's first 24 frames, 30 with SSAA,
and one more whenever a game drew something new later on. The modifier
volumes', the per-pixel renderer's last subpass and clear, and the
quads' pipelines were made the same way, when first drawn. Now no
pipeline is made in a frame (core/rend/vulkan/pso.h).

Measured with uprobes on lavapipe (2 CPUs), Jet Grind Radio from a save
state, 300 frames. "Cold" has no pipeline cache and an empty Mesa shader
cache; "warm" is the next run, with both kept.
                         pipelines made   first 24 frames    largest of them   context reset
                         in retro_run     cold     warm      cold     warm     cold    warm
  per-triangle, GPU TA,  35 -> 0         14350 ->  3412 ->   8331 ->  515 ->   3669 -> 3182 ->
  threaded                                9589 ms  1936 ms   4911 ms  315 ms   2561 ms 1223 ms
  per-triangle, CPU TA   35 -> 0          8614 ->  3659 ->   3679 ->  846 ->    969 ->  841 ->
                                          4007     2297      1600     428       277     427
  per-pixel, GPU TA      47 -> 0         21981 ->  6432 ->  12046 ->  866 ->   2682 -> 4429 ->
                                          9411     2768      4380     296      1602    2315
  per-triangle, SSAA 4x  59 -> 0         24992 ->  8182 ->  10083 -> 1445 ->   4828 -> 3467 ->
                                         13004     4363      4464     603      1913    1190
("Pipelines made in retro_run" counts the GLSL compiles and the
pipelines, the modifier volumes' and the quads' included.) The main
thread's own CPU time over the 300 frames is about the same (1289 ->
1144 ms in the first case). The largest cold frame
left is lavapipe's: it compiles a pipeline's code for its rasterizer when
the pipeline is first drawn with, on its own thread, and on two CPUs that
competes with the pipeline worker below.

Keys. A polygon's pipeline is kept under a 64-bit key of everything it
is made of, worked out from the polygon (PipelineManager::Key(),
OITPipelineManager::Key()), and every pipeline is built from its key
alone:
- per-triangle: the shader's 15 bits (vulkan/pso_key.h), Gouraud, SSAA's
  sample rate (none, at each sample, once a pixel), cull mode, list or
  strips, depth write, depth compare op, the stencil state (none, shadow
  bit set, shadow bit cleared, the two shadow redraws), blend enable, and
  source and destination instructions;
- per-pixel: the shader's 14 bits, Gouraud, sample rate, pass and
  subpass, cull mode, depth write, depth op, stencil state (none, set,
  cleared), blend enable, colour writes, source and destination.
The states are stored as they are made, not as the polygon's register
bits, so polygons that differ only in bits the pipeline ignores now share
one. Three key collisions in the old hashes are gone with this:
- the per-triangle key had no trilinear bit, so a trilinear polygon could
  take a non-trilinear polygon's shader, or the other way round;
- the per-pixel key had no blend factors for a two-volume polygon, and
  could not tell apart unsorted translucent polygons that write depth in
  the colour pass from sorted ones;
- the sorted-triangle topology came from a global setting read when the
  pipeline was made, and a change of the setting dropped every pipeline in
  the middle of a frame. It is now part of the key, and nothing is dropped.

The general pipeline. The first time a key is drawn, its pipeline is
asked of a worker thread, and the polygon is drawn at once with its
renderer's general pipeline. That is the same SPIR-V with the
specialization constant left at -1, so the shader reads the polygon's
bits from the push constants (the per-triangle block gets a seventh word
at offset 24, the per-pixel one a word at offset 96). Cull mode,
topology, depth test, depth write and compare op, and the stencil test,
ops, masks and reference are set with the draw (VK_EXT_extended_dynamic_state),
and so are blend enable, equation and colour write mask
(VK_EXT_extended_dynamic_state3). The core enables both extensions, and
VK_EXT_pipeline_creation_cache_control, on the device it makes when the
GPU has them. There is one general pipeline for each module and subpass:
2 for each per-triangle pipeline manager (the screen's and the
textures'), 8 for each per-pixel one (4 and 14 with SSAA). They draw the same pixels as each polygon's own pipeline: every
screenshot of the test runs drawn with only the general pipelines (a
test build) is the same, byte for byte, as with the polygons' own, with
both dynamic states, with the depth ones only, and with neither.

A device without the dynamic states has a general pipeline for each fixed
state it is drawn with. Those of the keys kept from earlier runs are made
at start. One not made yet is made there and then, the draw waiting for
it. That is the one case where a draw waits, once for each such state;
the log counts them. Without dynamic depth and stencil this covers cull,
depth and stencil states; without dynamic blend, the blend factors.

When. At renderer start (context reset), the general pipelines, the
modifier volumes' (all modes and cull modes), the depth prepass's (12,
24 with SSAA), the per-pixel renderer's translucent modifier volumes',
last subpass and clear, and the quads' are made by the drawing thread and the worker together, sharing out one
list: everything a frame can need. The polygons' own pipelines are made
on the worker, the draws' first, then all the keys kept from earlier
runs (prefetched). A new size of screen keeps every pipeline: the
per-pixel renderer used to drop its polygons' pipelines on every Init().

The worker runs below normal priority (nice 10 on Linux and Android,
below normal on Windows): what it makes is never waited for, and where
there are fewer processors than busy threads the frame goes first.

Lock-free. A slot's state is an atomic the worker publishes the pipeline
with (release; the draw reads it acquire). Requests go on two intrusive
lists (cMpscList: draws' and prefetches') and the worker sleeps on an
eventcount. A manager that starts again (render pass, samples) bumps a
generation, waits for the worker to answer what it was asked, and only
then destroys its pipelines. The worker drops stale requests without
building them. The tables are made with their manager; nothing is
allocated in a frame. Without threads (NO_THREADS=1) there is no worker,
and polygons are drawn with the general pipelines.

The cache. The worker has its own VkPipelineCache, made from the same
data as the context's, and both are created EXTERNALLY_SYNCHRONIZED where
the device allows, so the driver takes no lock for them. The file
(system/dc/vulkan_pipeline.cache) now starts with a header and the keys
of the pipelines made (PsoKeyLog), then the device's data. A file
without the header is read as before, as the device's data alone. It is
saved, with the worker's cache merged in, when the game is unloaded and
again when the context goes, if pipelines were made since. It is
written beside the old file and then renamed over it.

Draws. An untextured polygon drawn with a general pipeline needs a valid
texture binding, so each frame's descriptor sets bind one (the fog
table's view) as set 1 until a texture's replaces it. The per-pixel
renderer always writes the second texture binding (the first texture
when the polygon has one volume). The per-triangle renderer's trilinear
factor is pushed for every trilinear polygon; it used to be skipped when
it was 1.0, and the shader then multiplied by whatever was pushed last.
2026-10-15 20:24:11 +02:00
LibretroAdmin 1fa4c874db vulkan: the textures' images from a pool made when the renderer starts
A texture made its image when it was first used, and again when its
size or format changed: in the middle of the game, a scene's worth of
them in the frame it loads, and that was most of the time spent updating
textures.

rend/tex_pool.h keeps images by class - format, width, height (powers of
two from 8 to 1024) and whether it has its mipmap levels - with a free
list for each, threaded through the images' own entries: nothing is
allocated to take or give one back. How many of each class are made at
the start is a share of the video memory budget (a sixteenth of what is
left, 16 to 64 MB), spread the way games' textures are: mostly 16-bit,
square and mipmapped, 64 to 256 texels. A class with none left has one
made then, which stays in the pool: counted, and said once for that
class, as a sign the share is too small for the game. Only the thread
that renders touches it.

The Vulkan pool's images are optimal, sampled, copied to and from, and
storage images with a view of each level where the decoder takes their
format, so one image serves the CPU's upload, the kernel and the copy of
a planar texture alike. Their memory comes from the heap's blocks, which
are taken when the pool is made. A cache texture takes one in Init()
when the shape is one of the pool's; an image given up while frames that
may use it are in flight (DestroyLater(), a texture that becomes a
render target) goes back when the frame's place comes round again, one
whose texture has been deleted goes back at once. The renderer's own
pictures (fog table, palette, framebuffer, VMU screens) are not pooled.
The pool's summary is logged when the renderer goes.
2026-10-15 20:23:50 +02: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 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 6b41116a31 pvr, rend, maple, libretro: TA limits, punch-through, contexts, guns, RTT
- A vertex with x or y over 1e25 is dropped as upstream (Manic Panic
  Ghosts); one with a negative z is kept and clipped by the GPU (World
  Series Baseball 2K1). The test is on the bits, so fast-math keeps it.
- Punch-through polygons are tested on the shaded alpha (texture x
  vertex colour), not the texel's, in GL, GL4, Vulkan and Vulkan OIT
  (Langrisser Millennium), as upstream.
- Per-strip sorting: sorted translucent polygons write no depth there
  either (Sonic Adventure, which is forced to it, and LOW_END builds).
- A TA context unused for 60 renders is reused, as upstream; counted per
  render started, so frame skip does not change it, and never one of
  another frame size.
- NAOMI 2: the render-end interrupt always 1,500,000 cycles on.
- Light guns: at the gun's line and at vblank-in only SB_MDST is
  cleared; a transfer the game started ends at vblank-out with its answer
  and interrupt (RevolveR), one vblank started gets none (Virtua Cop 2).
  Ninja Assault, Mazan and Gun Survivor get the raw 640x480 position.
  The gun's X follows the widescreen hack when no widescreen cheat is
  on. A "Pointer" controller type aims and fires with one touch and
  reloads with two; the left stick aims on a gamepad in a gun game.
- Render to texture with a clip whose minimum is above 0: the target
  starts at the frame's pixel 0 and the clip is the scissor, in GL and
  Vulkan; only the clip goes back to VRAM, at its place, dithered by the
  pixel's place in the frame. Pixels past max - min were lost, and VRAM
  below the minimum was overwritten.

Save state V28: the gun's transfer owed its end, and the TA contexts'
ages (17 bytes). An older state gets ages 0.
2026-10-14 02:21:54 +02:00
LibretroAdmin 9389db363e rend: stride textures follow the stride; shared modifier volume list
- A planar texture's watched and hashed size was fixed when it was made
  while Update() read with the stride as it is now, and the stride is
  not in the cache key: changing the stride alone left a stale texture.
  The stride a texture was read with is kept and a change to it has the
  texture read again; one rendered to is left alone. (Upstream has the
  same issue. Not keyed on the stride: render-to-texture targets are
  made with StrideSel 0 and sampled with 1.)
- When a region's opaque and translucent modifier volume lists are the
  same list (Crazy Taxi, Daytona USA), the per-pixel renderers (GL4 and
  Vulkan OIT) draw the opaque volumes as the translucent ones (upstream
  85b8c599c; two empty lists do not count as shared).
2026-10-13 12:26:47 +02:00
LibretroAdmin b04783667e rend: empty clip hang, Vulkan swap chain growth, per-pixel discard, clips
- A render to a texture with FB_X_CLIP or FB_Y_CLIP max < min sized its
  target as a huge number; the power-of-two loop never ended. Such a
  render now draws nothing, as the chip does (GL, GL4, Vulkan, OIT).
- Vulkan: the frontend can lengthen its swap chain while running
  (fast-forward): the rings stay at the size of the start and what is
  kept per frame index grows with it (quad, OSD, framebuffer textures).
- Per-pixel renderers: no early discard for polygons whose source is the
  second accumulator or that have two volumes, and none for "destination
  times white source", which a shadow scales later.
- A polygon clipped inside its tile clip is also kept inside the pixel
  clip (upstream 4fdd28fab, done on pixel rectangles).
- Vulkan sorted triangles: odd triangles of flat strips took the next
  triangle's colour; they are emitted with the provoking vertex first.
- Vulkan: translucent depth mask (Cosmic Smash), as GL has it.
2026-10-13 11:08:04 +02:00
LibretroAdmin 31c2b79f8b rend: palette colours read at their centres, punch-through at the reference, precision
Precision lost by the renderers themselves, not by the chip:

A paletted texture drawn through the palette on the GPU read the 1024
colours at index/1023: on the edge between two of them. Which side it
fell was the GPU's rounding; with Mesa's software renderer 255 of the
1024 came out as their neighbour. They are read at their centres, in the
four sets of shaders. One constant, no work added.

A punch-through texel is kept when its alpha is at least the reference.
Both are 8-bit values, and floats by the time the shader compares them:
a texel equal to the reference could come out just under it and be
dropped (123 of the 65536 pairs with Mesa's software renderer). The
reference is sent half a step low.

OpenGL ES: colours, the fog colour and the samplers were lowp, which is
allowed to be 8 bits with nothing above 2: k/255 on a grid of 1/256, and
a palette index times 255 out of range. They are mediump. Where lowp is
already more, nothing changes.

The fog table's sampler interpolated between entries, and the shader
blends them itself: the neighbouring entry came in twice. Nearest.

The per-pixel OpenGL renderer's 565 render-to-texture target was a
16-bit one, so every blend into it was rounded to 5 or 6 bits. Eight, as
the other renderers have it; it is packed when it is copied to video
memory, as before.

tools/pvr/gles2_test.c: the fog table's sampler as the renderers have it.
2026-10-10 20:09:46 +02:00
LibretroAdmin 70c3d76f46 vulkan: a framebuffer drawn to for the first time starts as the border colour
The OpenGL renderers clear a framebuffer the game has not drawn to lately
to the border colour. The two Vulkan renderers cleared to black. They do
as OpenGL does, on the same frames, and black on the others as before.
2026-10-10 20:09:01 +02:00
LibretroAdmin d026bcf926 rend: the picture stays on the graphics card when the size changes in place
A change of size that the frontend has room for is made where the
renderer is, and the picture has to come along: a game that draws
part of the screen has the rest from it. It was read back to main
memory at the old size, flipped, and loaded and drawn again at the
new one - the road made for a new context, where nothing else is
left.

Here the context is the same, and the picture never needs to leave
the graphics card:

- OpenGL: the picture is in the frontend's buffer, in the corner
  that was the old size. It is scaled into the corner that is the
  new size, by way of a texture since the two overlap. Two blits.
- Vulkan: the image the last frame was drawn to is not deleted with
  the other old images. The next render starts from it as it does
  from a picture kept across a new context, at whatever size the
  screen now is, and it goes after that frame. Nothing is copied.

With the PowerVR2 filter on, OpenGL keeps the old way: the picture
drawn on is the filter's own, which is made again.

Besides, OpenGL no longer reads the picture back where it could not
be put back (OpenGL ES 2, which has no blit): the read was thrown
away.

Checked under RetroArch with a temporary switch in the core, not in
this patch, that starts at 2x and changes to 1x at frame 280, while
the disc draws only part of the screen: the picture was right after
it in every pass on gl, the per-pixel renderer's first on glcore,
seven passes on vulkan, four of those again under validation, and
the per-pixel renderer's first on vulkan under validation with
synchronization checks. The other way - a larger size inside the
room there is - goes the same code and was not run: the disc's
check of a mipmapped texture fails at 2x.

The headless test passes; the ordinary live test passes on gl as
far as it was run (every pass but the per-pixel renderer's).
2026-10-08 08:18:36 +02:00
LibretroAdmin 8a4c9a6325 vk: the screen's images outlive a change of size by a few frames
When the output size changes, the Vulkan renderers make new images
for the screen and destroyed the old ones there and then. The last of
the old ones had been handed to the frontend, which shows it again
for as long as it is given nothing newer - and can, before the first
frame at the new size is drawn. From upstream: a crash starting the
240p test suite with the libretro core.

They are put with the things to delete once the frames that could
still use them have gone by, as the renderers' other attachments
already are. Both Vulkan renderers.

The live test passes on vulkan, the per-pixel renderer with the
validation layer on. It has no change of output size in it.
2026-10-08 05:38:20 +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 39a506b9b1 rend: the screen's picture is kept when the context is destroyed and made again
A game that renders only part of the screen relies on the rest of
the picture staying as it was. Here the rest is in images that
belong to the graphics context, and going fullscreen, or changing a
video setting, destroys the context: the part the game was not
rendering came back black and stayed so until the game drew it again
- which on a loading screen or a menu can be never.

The picture is now fetched when the context is about to go
(rend/last_picture.h), and the first render to the screen in the new
context starts from it. In all four renderers:

- OpenGL, both: read from the framebuffer the frontend gave, and put
  into the one the first render is about to draw in. (OpenGL ES 2
  cannot put it back and is as it was.)
- Vulkan, both: copied from the image the last frame was drawn into,
  made a texture in the new context, and drawn in first where the
  last frame is drawn in for a render in part. The screen's images
  are made to be copied from, without the read-back memory other
  such images get.
- The first render to each framebuffer after a new renderer starts
  from an empty one, as before - unless a picture was kept for it.

One read of the picture for each context destroyed, nothing a frame.
A kept picture of another size is scaled to the new one's.

The test disc renders in part from frame 260 to its end again; the
last patch had it stop at frame 320 so that the fullscreen pass did
not come to depend on when its toggles fell. That pass now sends
three, the later ones while the disc renders in part, and the
per-pixel renderers have a fullscreen pass of their own. The core
says in its log when it keeps a picture, and the passes look for it.

With the putting-back switched off, the fullscreen pass fails on
OpenGL and on Vulkan (tried on the per-triangle renderers). With it,
the live test passes on gl, glcore and vulkan, both renderers each;
the Vulkan passes also with the validation layer on. The headless
test passes.
2026-10-08 03:21:44 +02:00
LibretroAdmin 96c9916c26 vk: a render that covers part of the screen leaves the rest of the picture
A game does not have to render the whole screen: with the pixel clip
or the region array it can render a part, and the console leaves the
rest of the framebuffer as it was. Games do it for loading screens
and for menus where one corner moves.

The Vulkan renderers draw every frame into a fresh image, cleared,
so the part that was not rendered came out black - on the screen for
as long as the game went on rendering in part. (The OpenGL renderers
draw over the last picture and had it right.)

When a render will not cover the screen, the last frame is now drawn
into the new image first, whole, and the render goes over it:

- per-triangle: in the render pass, before the first polygon, with
  the quad pipeline the VMU screens are drawn with.
- per-pixel: in the colour subpass of the frame's first pass, with a
  quad pipeline of its own for that subpass, and what the frame had
  bound is bound again after it.

A render that covers the screen - nearly all of them - does none of
this, and a framebuffer the game has not drawn to lately still
starts empty.

The test disc renders only the left 200 pixels of its screen from
frame 260 to frame 320, with a white polygon drawn nearest over most
of the right-hand side. Everything on the right has to stay as the
last whole frame drew it, and the white polygon must not appear. On
the old code the Vulkan renderers show the right-hand side black.

The live test passes on vulkan, both renderers, with the validation
layer on, and on gl and glcore; the headless test passes.

Not covered: the part of the picture that is kept does not survive
the context being destroyed and made again (fullscreen), on any
renderer - it is black until the game renders it again. That is why
the disc stops rendering in part after frame 320, before the test's
fullscreen pass could look at it.
2026-10-07 20:03:57 +02:00
LibretroAdmin 7c4756e4ac vk: a texture that becomes a render target gets an image that can be rendered to
When a game renders to a texture at an address where the cache
already has a texture of the same size and format - one the game
uploaded there earlier - the Vulkan renderers rendered into that
texture's image as it was. It was made to be sampled and copied to,
not to be rendered to: what happens then is up to the driver, and
since the last patches it could not be copied from either, which is
how a render gets back to video memory. From upstream, where it
shows as a validation error on Virtua Tennis's player list.

A texture records what its image was made for, and the image is made
again when that is not what a render target needs.

The live test passes on vulkan with the validation layer on. The
case itself - an uploaded texture turned render target - is not on
the test disc.
2026-10-07 19:50:52 +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 783264f91c libretro: "Enable RTT (Render To Texture) Buffer" is gone, and the table of games that forced it
The option chose between two ways of treating what a game renders to
a texture: copy it to video memory after every such render, waiting
for the graphics card each time and losing Render To Texture
Upscaling, or leave it on the card and have the games that look for
it in video memory come out wrong. A table switched the first on for
the games known to need it.

Since the last three patches there is one way, and it is right for
both: the picture stays on the card and video memory gets it when it
is first touched there. Nothing is left for the option to choose.

- The core option, in every language, and settings.rend
  .RenderToTextureBuffer.
- The copy-after-every-render paths of all four renderers, with what
  only they used: the Vulkan drawers' second render target and its
  render pass variant, the check for the setting having changed, and
  WriteTextureToVRam() (rend/rtt_watch.cpp packs pixels now).
- The table's column, in the console's table and the arcade one, and
  the 51 rows that had nothing else in them.

Render To Texture Upscaling stays, and now applies to every game.
A frontend that still has the old option saved ignores it.

The live test passes on gl, glcore and vulkan, the headless test
passes.
2026-10-07 19:36:06 +02:00
LibretroAdmin d359c881bb vk: a render to a texture reaches video memory when the game touches it there
The last patch did this for the OpenGL renderers (rend/rtt_watch.h);
this is the same for the two Vulkan ones. What a game renders to a
texture stays in the texture it was drawn into, the pages of video
memory it belongs in are closed, and the first thing to touch them
has the picture fetched and written there as the console packs it.

- core/rend/vulkan/rtt_read.{h,cpp}, used by both renderers: the
  picture is copied to memory this side can read with commands of
  its own, in a command pool of its own - a read can be asked for in
  the middle of a frame being set up - and waited for. Finer than
  the console's, it is first made the console's size on the card.
- The texture a render goes into can be copied from (one more usage
  flag when it is made).
- What is waiting in the memory a new render covers is dealt with
  before that render touches its texture, while the old picture is
  still what it was.
- Before the texture cache is emptied, what is waiting goes to video
  memory; when the renderer goes, it is dropped.
- rend/rtt_watch: the renderer's name for a picture is as wide as a
  pointer, and everything waiting can be asked for at once.

The live test holds the Vulkan renderers to the disc's check again
(the last patch left it out for them). The program accepts either
rounding of the orange it reads back: 32 of 63 for its green, or 31,
which is what Vulkan here makes of a half.

With "Enable RTT (Render To Texture) Buffer" on, all four renderers
still do what they did; with it off, which is the default, all four
now do this. The option and the table of games that force it on have
nothing left to do that this does not, and go next.

The live test passes on gl, glcore and vulkan (both Vulkan renderers,
threaded rendering on and off, states saved and loaded); the headless
test passes. A run with the Vulkan validation layer on went through
the read-back without a message from it.
2026-10-07 18:38:26 +02:00
LibretroAdmin 4f4db17cf2 vk oit: the picture is put together over all of it, not the part the game clips to
The last step of the per-pixel Vulkan renderer writes the finished
picture, and did it under the scissor the game's clipping gives.
Outside that the image it writes to was left as it was - and it
writes to a different image of two or three every frame, so what was
left was two or three frames old and changed every frame. From
upstream: the loading screen before a fight in Soul Calibur blinking.

It is written under the whole viewport, and so is the clearing of
the per-pixel lists after it.
2026-10-07 17:20:54 +02:00
LibretroAdmin a2ad6ed32b rend: a modifier volume is clipped to the rectangle it was sent under
A polygon sent under a clipping rectangle, asking to be kept inside
it, is. A modifier volume was not: its rectangle was never recorded,
and its shadow fell wherever the volume reached. Upstream: shadows
running out of the baserunner cameras in World Series Baseball 2K1.

The volume now carries the rectangle and mode it was sent under, as a
polygon does, and all four renderers - both OpenGL ones, and Vulkan
per triangle and per pixel - draw it under that scissor. (Kept inside
only; "kept outside" is not done for volumes, here or upstream, which
has this for its two per-triangle renderers.)

The test disc has a new polygon under a volume that covers its width,
the volume sent under a rectangle that is the polygon's left half:
grey on the left, white on the right. With the clipping switched off
the live test fails on the right half (127, not 255). It passes on
gl, glcore and vulkan, per-pixel included.
2026-10-07 15:16:44 +02:00
LibretroAdmin 2d161a2983 rend: the transforms without glm
The renderers used glm for one thing: the three transforms a frame is
drawn with - guest coordinates to clip space, to the viewport, to the
scissor rectangle. Each is a scale and then a move along each axis and
never anything else, and they were built as 4x4 matrices by
multiplying 4x4 matrices together.

core/rend/xform.h, plain C89, keeps a transform as the six numbers
that are not 0 or 1. Putting two together is three multiplications and
three multiply-adds in place of sixty-four multiplications; a point
goes through one with a multiply-add an axis. A shader still gets a
4x4 matrix, written out from the six. The Vulkan uniform and push
constant structures hold plain arrays of the same size and layout.

Same floats: the old class and the new were run side by side on
600,000 random frames - render to texture or not, any clip rectangle,
screen size, scaling, depth scale - and every element of all three
matrices, and a point and a size taken through each, came out the
same. (That needed glm; tools/rend/xform_test.sh, which stays, checks
the transforms against 4x4 matrices multiplied out in full.)

Code: setting the three up is 324 instructions where it was 666, and
measured on its own about thirteen times as fast (20 million set-ups:
1.16 s to 0.09 s). The six objects that used glm are 1458 instructions
smaller together; the one that grew, the Vulkan drawer, grew by which
of vulkan.hpp's exception constructors the compiler chose to emit, not
by this.

glm had also been bringing <cmath> into four files that did not ask
for it; they do now.

The live test passes on gl, glcore and vulkan, per-pixel renderers
included.
2026-10-07 14:23:01 +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 7b081630db vulkan per-pixel: growing the buffers leaves alone what frames in flight use
When the picture grows, the per-pixel renderer makes a larger image for
its per-pixel list heads. It destroyed the old one and rewrote the
descriptor set to name the new one, after a wait that was meant to make
that safe: context->WaitIdle(), which waits for the frontend's frames.
The frames that use the image and the set are the core's own, which it
submits itself and waits for one ring slot at a time. They could still
be on their way through the GPU.

The old image and the old descriptor set are now retired through the
command pool, the way the drawer's other attachments already are, and
destroyed when the frames that could use them are done. A new set is
allocated for the new image. Nothing waits.
2026-10-06 05:23:31 +02:00
LibretroAdmin e34d0028e4 remove the Fog Effects and Volume Modifier core options
Fog and modifier volumes are not enhancements: they are what the
PowerVR2 does, and a game that uses them looks wrong without them, with
no fog where there should be fog and no shadows where there should be
shadows. Both options defaulted to on and there is no reason to offer
"off".

The two options go, and settings.rend.Fog and
settings.rend.ModifierVolumes with them. The OpenGL, OpenGL per-pixel
and Vulkan renderers (both Vulkan drawers) always use the fog mode the
polygon asks for, always keep the fog table up to date, and always draw
the opaque and translucent modifier volumes.

The translated option tables still carry the two entries until they are
next regenerated; they are looked up by the English table's keys, so
they are not shown.
2026-10-06 03:07:31 +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
libretroadmin 45bd2f4e59 vulkan: defer attachment deletion instead of WaitIdle on resize
When an RTT/OIT framebuffer attachment must be grown, the renderer called
GetContext()->WaitIdle() before recreating it, stalling the whole device to
ensure no in-flight frame still referenced the old attachment. Replace this
with a per-frame retire list on CommandPool: the old attachment is moved into
a slot-indexed deferred-delete queue and destroyed in BeginFrame once that
slot's fence has signaled (i.e. the GPU has finished the frame that used it),
which is exactly the guarantee the existing N-buffered command sync already
provides. Removes the full-device idle from the depth/color grow paths in the
screen and RTT drawers and from OITDrawer::MakeBuffers.

The OITBuffers pixel-buffer realloc keeps WaitIdle for now: it has no
CommandPool reference and is a cold resolution-change path.
2026-07-06 08:05:22 +02:00
twinaphex dd4aec7e3f Backport https://github.com/flyinghead/flycast/commit/0e20d5976bcb75b2cb69307e3e872b16c645be50 2021-03-05 19:42:11 +01:00
Flyinghead c2f735809c handle some palette textures on the GPU. vulkan: some linear tiling
get rid of PixerPacker templates
gl4: no need for GLSL precision qualifier in GL4
vk: Refactor base and oit renderers to avoid code dup
vk: Linear tiling is faster for small, frequently updated textures
Fix Hello Kitty crash with empty first TR polygon
Fix Test Drive Le Mans crash on invalid textures
2020-07-11 19:19:58 +02:00
Flyinghead fb7d27704b vulkan oit: pass native blend function values to shader, not vulkan ones
Fixes missing texture in Plasma Sword start screen
2020-02-07 17:07:50 +01:00