40 Commits
Author SHA1 Message Date
LibretroAdmin e7085e20fe Vulkan: fix the 32-bit builds
On 32-bit targets Vulkan-Hpp handles have no implicit conversion to
their C type, so hand the pipeline cache to hdr_vk_new() with an
explicit cast.
2026-10-31 12:43:37 +01:00
LibretroAdmin 8c2e5a410e Vulkan: make the fixed and utility pipelines on the pipeline thread
A renderer hands its general, modifier volume, prepass and other fixed
pipelines to the pipeline thread when it starts, and no longer waits for
them; a draw takes one made, makes it itself if the thread has not
started on it, or waits for the thread if it has. The quad, SSAA resolve,
motion vector, texture decoder, TA decoder and compute renderer
pipelines go the same way, and one the thread could not make is made
where it is first used, failing there as it did at start. The compute
renderer's kernels are handed over first, as every frame uses them.
2026-10-28 08:23:04 +01:00
libretroadmin 9140c5f71a HDR: post effects - bloom, halation, chromatic aberration
The core options of beetle-psx's HDR post effects: HDR Bloom (and its
Selection, Range and Convolution), HDR Halation, HDR Chromatic Aberration
and HDR Post: Compact Intermediates. They are shown with an HDR Color
Format, on Vulkan, Direct3D 11 and Direct3D 12, every renderer of each.
With them on, the encode is three steps on every backend. First the
picture, or SSAA's channel sums, goes into scene-linear light in a
half-float image of its size (or 11/11/10 with Compact Intermediates,
where the device draws, blends and filters it). Then the bloom is built
from that image by the passes of one plan, hdr_post_plan_make(): the
bright pass, then either the two-band blur or the six-octave pyramid.
Last, the composite adds the aberration and the tinted bloom and encodes
as before. The shaders are beetle-psx's, in GLSL and in HLSL (one DXBC
for both Direct3D backends). What they need is made the first time they
are on, and the rasterizers' fused HDR subpass is left undrawn while
they are. Off, none of it runs, and nothing of it exists.

tools/hdr/hdr_post_ref.c runs the same three steps on the CPU from the
24-bit picture, rounding each image to its storage format and tapping
where the shaders tap, and reads the options the way the core reads
them. vk_hdr --post holds the scRGB picture to it:
 - tools/vulkan/vk_hdr.sh runs every Vulkan renderer, with and without
   SSAA, and each setting, and checks that other settings than the
   picture's fail;
 - tools/hdr/d3d_hdr.sh runs the Direct3D core;
 - tools/hdr/hdr_d3d.sh post11 and post12 run the Direct3D 11 and 12
   encoders under wine.
vk_sanitize.sh runs them under ASan and UBSan.
2026-10-25 07:06:04 +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 0d947c9797 HDR output: Color Format (24-bit, HDR10, scRGB) and Display Transform
A Color Format core option hands the frontend 30-bit HDR10 or 64-bit
scRGB instead of the 24-bit picture, on Vulkan, Direct3D 11 and Direct3D
12 and every renderer of each, through the libretro HDR API (the pixel
formats, GET_DISPLAY_METADATA, GET_HDR_METADATA). The picture is placed at
the frontend's paper white and mapped onto the display through a Display
Transform (beetle-psx's, vendored in rend/hdr/: Reinhard knee by default,
which keeps everything below paper white as it was, ACES, ACES 2.0, AgX,
PBR Neutral, GT, Hable and others), with optional HDR Expansion. At
24-bit, or when the frontend refuses, none of it runs and the picture is
the same to the byte. On Vulkan the encode is the last subpass of the
frame's render pass on the rasterizers and one draw on the compute
renderer and CPU framebuffers, recorded into the frame's own submission
(after the chip's write-out on a frame that has one);
on Direct3D 11 and 12 it is one draw in the frame's context or command
list, from one shared DXBC blob (tools/rend/kernels_dxbc.sh update-hdr).
Nothing is copied and no submission or wait is added. ACES 2.0 (Full)'s
cube is baked off the frame by a worker thread handed its jobs through
atomics and an eventcount, without locks.

tools/vulkan/vk_hdr.sh runs every Vulkan renderer at each Color Format
and judges the pictures; tools/hdr/d3d_hdr.sh does the same with the
Windows build on Direct3D 11 under wine; tools/vulkan/hdr_hlsl.sh and
tools/hdr/hdr_d3d.sh hold the HLSL, and the Direct3D 11 and 12 encoders
under wine, to the GLSL over every curve and setting; tools/hdr/hdr_cube.sh
holds the worker's cube to the cube baked in place, under TSan and
ASan/UBSan. All of them run in the Vulkan negotiation workflow.
2026-10-24 22:45:23 +02:00
LibretroAdmin b7e3a3250c vulkan: a renderer resized or taken down waits for the frames still on the GPU, however long they take
CommandPool::WaitAll(), the wait before a renderer's attachments, query
pools and the rest are destroyed or made again (a change of internal
resolution or SSAA, the renderer taken down), gave up after a second so
as not to hang on a frame that was begun and never submitted - and then
went on to destroy what a frame that was simply slower than that was
still using. On lavapipe, at twice the resolution with SSAA, the
validation layer reported it: "vkDestroyQueryPool(): can't be called on
VkQueryPool ... that is currently in use by VkCommandBuffer"
(VUID-vkDestroyQueryPool-queryPool-00793), from GpuFrameTimer::Init() in
OITVulkanRenderer::Resize(). A slow GPU at a high internal resolution
would read freed memory the same way.

When the fences do not all come within the second, the queue the core
and the frontend share is now waited idle, under the queue lock
(VulkanContext::WaitQueueIdle(), which ReleasePicture() uses too): that
finishes a slow frame and does not hang on one never submitted. It is
only ever on a resize or a renderer going, never in a frame.

On lavapipe with the validation layer, Jet Grind Radio, per-pixel
sorting, sixty option changes (internal resolution 1x <-> 2x, SSAA off
<-> 2x, TA processing CPU <-> GPU, per-pixel <-> per-triangle, Rendering
Method), half of them while paused: with threaded video three such
errors before, none after; none unthreaded either. The error came in
one run and its absence is one run each, so this is evidence, not proof.
2026-10-24 13:21:44 +02:00
LibretroAdmin 2cec1266a1 vulkan: the frontend lets go of the picture before a renderer is taken down
Changing the Rendering Method (rasterizer <-> compute), the alpha
sorting or anything else that has the renderer made again while a game
runs destroyed the old renderer's pictures - the images and views last
handed to the frontend with set_image - while the frontend could still
draw them. RetroArch draws the last picture again for as long as no new
one comes (paused, behind its menu, on a frame the game draws nothing
in), and with threaded video its video thread draws frames the core
thread has already moved past. The renderer's Term() only called
wait_sync_index, which waits for the index the core draws into next and
is a no-op in RetroArch's own driver. RetroArch's video thread then
built its filter chain's descriptors from a destroyed image view: an
access violation in the NVIDIA driver under slang_pass_build_commands,
reported on Windows with threaded video on.

Before the renderer destroys anything, the frontend is now told to let
go of its picture with set_image(NULL) - RetroArch's threaded video
draws what it has queued and waits for its own submissions then - and
the queue the frontend and core share is waited idle, under the queue
lock, for a frontend that only forgets the image. It happens only when a
renderer is taken down; no frame waits.

On lavapipe with the validation layer and threaded video, switching the
Rendering Method while running and while paused, Jet Grind Radio: before,
"vkUpdateDescriptorSets(): Invalid VkImageView" from RetroArch's filter
chain and a crash at the seventh switch; after, twelve switches with no
validation error.
2026-10-24 10:18:42 +02:00
LibretroAdmin 593d919328 vulkan: SSAA's per-pixel lists test a translucent fragment at its samples
With SSAA the per-pixel renderer puts a translucent fragment in its lists
once a pixel, with the samples it is seen at, and tested each sample the
polygon covers against the opaque polygons' depth there with the
fragment's depth at the pixel's centre, which it also kept and sorted it
with. Where a triangle only clips the pixel, the centre's depth is the
plane extrapolated past the triangle: samples were tested with the wrong
depth, and the fragments of a model's neighbouring polygons were sorted
before or after each other wrongly at its edges - the dotted line of
wrong colours Direct3D 12 drew along the hem of the skater's jacket in
Jet Grind Radio (fixed there in "dx12: SSAA, as the Vulkan renderers have
it"). Vulkan was not seen doing it only because lavapipe shades a shader
that reads a sample of an input attachment at each sample, with
gl_FragCoord at the sample; a GPU shades it once a pixel, as asked.

Now (vulkan/shaders/oit_sample_depth.glsl):

- Where the device has VK_EXT_post_depth_coverage (enabled when the core
  makes the device), the lists' pass takes the samples the device's own
  depth test passed: the pass already tests greater or equal against the
  opaque depth, read-only, and early fragment tests with post-depth
  coverage put that test before the shader and its result in
  gl_SampleMaskIn. That is the test the shader did by hand, done by the
  device that wrote the depth, so it is exact at every sample, and a
  fragment hidden at all of them is not shaded.
- Elsewhere each sample is tested by hand with the depth there: the depth
  is a plane across the screen, so gl_FragCoord.z and its differences to
  the next pixels (dFdxFine/dFdyFine, taken before anything is discarded)
  give it at any sample, from where gl_FragCoord is (the centre when shaded
  once a pixel; the sample itself where a device shades it at each sample,
  so lavapipe's depth there is its own, untouched). That plane comes out a
  few units in the last place off the device's own depth at a sample (5
  at most on lavapipe), so a polygon drawn again on the opaque one's plane
  would lose a fifth of its samples at random; a depth within 16 units of
  2^-23 of the opaque one counts as equal.
- Either way the fragment is kept and sorted with its depth at the
  centroid of the samples it is seen at, which is in the triangle; the
  centre's where it is seen at all of them (the standard patterns are
  centred), as before.

Without SSAA nothing changes: the modules of one sample are the same
words (tools/rend/kernels_spv.sh), and at one sample gl_FragCoord.z is at
the only sample, the centre, which the fragment exists only for covering:
never extrapolated, and to the bit the depth test's
(tools/rend/oit_depth_test.sh, the next commit). Two modules more, made
when the renderer starts and only on a device with the extension; the
pipeline keys are as they were; nothing compiled, allocated or waited for
in a frame. Direct3D 11 and the OpenGL 4 per-pixel renderer have no SSAA
and keep the centre's depth, which is the sample's.

The log says which way the renderer takes: "Vulkan per-pixel renderer:
with SSAA, a translucent fragment takes the samples the depth test passed
(VK_EXT_post_depth_coverage)".
2026-10-23 10:56:19 +02:00
LibretroAdmin 40eee790bb vulkan: the compute renderer's textures indexed as non-uniform
The compute renderer's walk shades the pixels of a tile one a lane, and
each lane's pixel has its own polygon, so the slot of the array of
textures it samples differs between the lanes of a subgroup. The Vulkan
kernel indexed the array with it as a plain index, which Vulkan allows
only for an index that is the same in all of them (dynamically uniform):
with lanes that differ, the device may sample every lane's texel from one
lane's texture. Lavapipe does, eight lanes at a time: at 4x SSAA Jet Grind
Radio's glass wall with its graffiti is blocks of other textures, the
score's digits on another frame are a block of another texture, and
pixels along the edges where two textured polygons meet take the other
one's texels. What a GPU does with it is up to its compiler (AMD's
takes one lane's index for the whole wave). The Direct3D 12
kernels, made of the same header, have always had NonUniformResourceIndex
there, and do not show it.

The index is now marked non-uniform (GL_EXT_nonuniform_qualifier),
which needs shaderSampledImageArrayNonUniformIndexing: the core asks the
device for it, with VK_EXT_descriptor_indexing, when it makes the device.
For a device without it, a second walk takes the textures in turns: the
lanes with the first active lane's slot sample, then the next slot's,
until every lane has its texel (Vulkan 1.1's subgroup ballot,
PVRC_TEX_WATERFALL: SPIR-V 1.3). Both are made when the renderer starts,
as before, and give the same picture, pixel for pixel; a device with
neither is left to the raster renderer, as one without the other features
the compute renderer needs is.

The raster renderers, the texture decoders, the GPU-side TA decode and
the OpenGL, Direct3D 9 and Direct3D 11 shaders index no array of
textures, and the Direct3D 12 shaders index theirs either with a
draw's constant (the same in every lane) or with NonUniformResourceIndex:
none has the same fault.

tools/rend/kernels_spv.sh makes the second walk too (compute_spv.inc);
the DXIL of the compute kernels is the same, only the sums of their text
are new. tools/rend/tile_test.sh enables the feature on its device, and
TILE_WATERFALL=1 runs the other walk.
2026-10-21 07:09:47 +02:00
libretroadmin d15f4cfc29 vulkan: create the device through the frontend (negotiation v2)
Where the frontend supports Vulkan context negotiation v2, the core
offers create_device2 and makes its device with the frontend's
vkCreateDevice wrapper, which adds the extensions and features the
frontend wants - in RetroArch, VK_NV_low_latency2 or VK_AMD_anti_lag for
Driver Pacing, which a device the core makes itself never has (Driver
Pacing: "Off: the core made the device without it"). A frontend without
v2 is offered version 1 as before.

tools/vulkan/vk_negotiate.sh negotiates the device both ways under the
validation layer, as RetroArch (where the device must be made through
the wrapper) and as a frontend without v2; the new Vulkan negotiation
workflow runs it on lavapipe.
2026-10-19 21:26:20 +02:00
LibretroAdmin 2bad16db05 vulkan: 64-bit integers and indexed texture arrays in shaders where the device has them
shaderInt64 and shaderSampledImageArrayDynamicIndexing are enabled when
the device supports them, for the compute renderer's edge arithmetic
and its array of textures; nothing else uses them.
2026-10-16 02:01:25 +02:00
LibretroAdmin 0311970808 vulkan: no GLSL compiler in the core
Nothing in the core compiles GLSL any more: every Vulkan shader is SPIR-V
made with the source (tools/rend/kernels_spv.sh). glslang is no longer
built into it. Its sources stay in core/deps, unused. That is 43 C++
files fewer to build (42 of glslang's and compiler.cpp), and the Linux
x86-64 core goes from 38.3 MB to 36.2 MB. The Vulkan headers' include
path (core/deps/khronos) is kept.

SYSTEM_GLSLANG=1, which pointed the build at a system glslang, has
nothing left to do and is gone.
2026-10-15 20:24:16 +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 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 79d73fc64f vulkan: indirect draws of several at once, with firstInstance and a count
When the core makes the Vulkan device (the frontend's create_device), it
now also enables, where the GPU has them:
- multiDrawIndirect: one vkCmdDrawIndexedIndirect() of many draws;
- drawIndirectFirstInstance: a firstInstance other than 0 in the
  draws' arguments;
- VK_KHR_draw_indirect_count: the number of draws read from a buffer
  (vkCmdDrawIndexedIndirectCountKHR(), loaded with the device).

VulkanContext says which there are, and maxDrawIndirectCount. A device
the frontend made by itself has none of them. Nothing uses them yet;
GPU-side TA processing's draws will.
2026-10-15 13:48:58 +02:00
LibretroAdmin 9fcfec2935 vulkan: textures made on the GPU from their bytes in video memory
A texture used to be converted on the CPU, texel by texel, into the
upload ring, and copied from there to its image. Now its bytes are
copied as they are - the texels, a compressed texture's codebook, the
part of the palette a paletted one uses - into a ring of their own, and
the compute kernel of rend/texdecode.h makes the texels from them
straight into the image: no conversion on the CPU, and for most formats
less to copy (a compressed texture is an eighth or a sixteenth of its
texels, a paletted one a quarter or an eighth, YUV half). The texels are
the CPU's to the bit, and the images have the formats they had.

A planar 16-bit texture in a format the image can have its texels in as
they are - 565 always, 1555 as A1R5G5B5, 4444 as B4G4R4A4 with the
view putting the channels back - is not converted at all: its rows are
copied to the image as they are, with the stride as the row length.

The 16- and 8-bit images are written through views of 16- and 8-bit
integers (a mutable format, extended usage, Vulkan 1.1), which needs the
shaderStorageImageExtendedFormats feature: the device is now made with
it when the GPU has it. What a device cannot make an image of, and
everything on a device that is Vulkan 1.0, is converted on the CPU as
before.

What a frame asks for is recorded once, at the end of Process() and
before the frame's draws, in one command buffer of the texture pool's:
one barrier for all the images, the copies, the dispatches a kernel at a
time, one barrier for all of them again. The kernels, the ring (sized
from the video memory budget: 2 to 16 MB a frame), the descriptor pools
and every list are made when the renderer starts, and nothing is
allocated or compiled while the game runs; a frame that asks for more
than there is room for has the rest converted on the CPU. A frame's
piece of the ring and its descriptor sets are used again when its place
comes round, after the texture pool's fence for it, as the upload ring's
are.

A texture made on the GPU and then on the CPU in the same frame, or
going away, or rendered to, has its job taken back. A texture updated
from GetTexture() now takes a command buffer from the pool only when it
records something: one made on the GPU records nothing of its own, and
neither does one that turns out not to be valid, which used to have an
empty command buffer submitted.
2026-10-15 13:48:11 +02:00
LibretroAdmin 2820d66640 rend: what video memory each API says there is to use
SSAA and the tiled renderers will size their pools once, when the
renderer starts, against what the device can give, and never grow them
mid-game. Each renderer's context now reads that budget when it is set
up, keeps it (GetVramBudget(), gl.vram_budget; rend/vram_budget.h) and
logs it in one line. Nothing sizes anything by it yet.

- Vulkan: VK_EXT_memory_budget, the largest device-local heap's budget
  and what is already used of it. It is a physical device query, so
  the extension need not be enabled on the device the frontend made;
  without it (or without vkGetPhysicalDeviceMemoryProperties2), half of
  that heap.
- Direct3D 11: DXGI 1.4's QueryVideoMemoryInfo() on the adapter's local
  segment; before Windows 10, the adapter's dedicated memory, or half
  its shared memory when it has none. The function takes any DXGI
  adapter, for a Direct3D 12 renderer to use as well.
- OpenGL and OpenGL ES (both GL renderers, through findGLVersion()):
  GL_NVX_gpu_memory_info, else GL_ATI_meminfo's free texture memory,
  else a fixed 512 MB (256 MB for OpenGL ES). The extension list is
  now read to its end once at start-up, and the error its end leaves is
  cleared.

All of it runs once per context, none per frame.
2026-10-15 05:07:19 +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 5274546c9d vulkan: memory a smaller render size no longer needs goes back to the driver
The heap takes device memory in 64 MB blocks and gave an empty block
back only when an allocation found no room anywhere. Turn the internal
resolution up and down again and the blocks the large images were in
stayed with the core for the rest of the session, empty.

Thirty-two frames after the renderer changes size - by when the frames
that used the old images are long done with, and the images freed (two
frames, with Mesa's software Vulkan) - the blocks that are empty are
given back, all but the last of each kind (memory type; linear or not),
so that the next allocation of every kind is still an offset and not a
trip to the driver. vk_heap_trim_spare() is the new call; once, not every
frame: a block that comes empty in ordinary running is left alone.

Run in RetroArch on Mesa's software Vulkan with the test disc, the render
size taken 1x, 4x, 1x, 4x, 1x, 2x by a line of test code since removed:

  per-triangle  192 MB reserved at 4x; back at 1x, 14 MB in use and 192
                reserved before, "64 MB given back, 128 MB reserved" now,
                both times
  per-pixel     384 MB at 4x; back at 1x 320 MB, both times (its pixel
                buffer stays at the size it grew to)

and the picture at the end is the same file with the trim and without, in
both renderers.

tools/vkheap: the three empty blocks beside one in use go back; with
none in use one of each kind stays, and the next allocation does not ask
the driver.

(Throughput item 4 of the audit, the part of it that can be seen here.
Its budgets per memory heap and a smaller first reservation - 128 MB are
taken at the start for the 14 to 22 MB this disc uses - are not in this.)
2026-10-09 07:16:39 +02:00
LibretroAdmin b5da96cd78 vulkan: VMA is gone
The renderer's three allocation sites - textures, framebuffer
attachments and buffers - now go through vk_heap, and vk_mem_alloc.h and
its wrapper are out of the tree. The core is 182 KB smaller.

core/rend/vulkan/heap.{h,cpp} is what the renderer's classes hold: the
Allocation that gives its memory back when it goes, as before, and
VulkanHeap in place of VMAllocator. An allocation's pointer comes with
it, so MapMemory() is a load.

What each kind of memory asks for:

 - textures and attachments: device-local, falling back to whatever the
   image allows; a transient attachment first tries memory the device
   only backs if it has to
 - buffers and images the CPU fills: host-visible and coherent, on the
   device where the device has such memory. VMA was asked for
   host-visible only and nothing ever flushed, which was wrong on a
   device whose host-visible memory is not coherent
 - buffers the CPU reads back (render-to-texture readback): host-visible
   and host-cached. VMA put these in device memory seen through the bus
   on a discrete card, which is write-combined and slow to read

One block for images and one for the CPU's buffers are taken when the
device is set up, so a frame gets memory as an offset rather than from
the driver. A block is 64 MB, or an eighth of the device's memory where
that is less.

tools/threads/live.sh passes on the Vulkan renderer: the screenshot, the
save-state comparison with non-threaded rendering, and save, load and
reset back to back. A run under the Khronos validation layer reports
nothing. Software Vulkan only; not run on a GPU.
2026-10-05 17:32:12 +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 63c599caa2 vfs: route all file I/O through libretro VFS
Vendor the current libretro-common VFS stack -- streams/file_stream.c,
file_stream_transforms.c, vfs/vfs_implementation.c and their headers -- and
wire RETRO_ENVIRONMENT_GET_VFS_INTERFACE in retro_set_environment via
filestream_vfs_init, so the frontend's VFS handles every file operation when
provided, with libretro-common's local implementation (UTF-8-safe on every
platform) as the fallback. This supersedes the fopen_utf8 spot-fix from the
previous commit with the proper single I/O layer.

Converted to filestream: coreio (all disc content: cue/gdi/cdi tracks and the
gdi/lst text parsing), the mds/mdf reader, libchdr via its coretypes.h porting
shim (core_file becomes RFILE; SEEK_* and RETRO_VFS_SEEK_POSITION_* share
values so whence passes through; chd.cpp owns the handle since chd_open_file
does not), BIOS/flash (flashrom.h), VMU/eeprom/JVS dump (maple_devs), the REIOS
font, the Vulkan pipeline cache, the m3u parser, and the NAOMI .lst/.bin
probes. The two stat() checks in retro_load_game were already replaced with
path_is_directory.

Bug found and fixed by the conversion: mds.cpp passed its raw FILE* as a
core_file* to RawTrackFile -- a type pun that made core_fread interpret stdio
internals as coreio's wrapper struct, i.e. mds/nrg image reading has been
broken; it now opens the mdf through core_fopen like every other format.

Version-skew fallout from syncing libretro-common: file_path.c split
(file_path_io.c added, with rtime.c and current stdstring), libretro.h updated
to current (additive; provides the retro_vfs_*_t typedefs incl. stat_64), and
the removed fill_short_pathname_representation replaced by a local helper over
fill_pathname_base + path_remove_extension.

Exceptions, by design: the NAOMI ROM cache maps files with mmap through native
fds (no VFS equivalent); files opened by name inside vendored libzip/lzma
(NAOMI zip/7z) keep their internal opens; debug-only dumps (modem, pcapng,
track dumper) unchanged.

Verified: clean Linux build; VF3tb from an accented path and DOA2 load and run
(content, BIOS, tracks via filestream); VMU create/write and flash save
exercised (deleted VMU recreated correctly).
2026-07-04 07:48:52 +02:00
libretroadmin 344ca696b4 paths: open files as UTF-8 on Windows (fixes accented paths)
RetroArch hands the core UTF-8 paths, but every file open in the core went
through plain fopen(), which on Windows interprets the ANSI codepage -- so any
path with non-ASCII characters (C:/Emulación/...) failed to open: content load
crashed and the BIOS fell back or failed, exactly as reported in
libretro/flycast#108 back in 2018 and still present until now.

libretro-common's compat/fopen_utf8 (already compiled into the core, just never
used) converts UTF-8 to UTF-16 and opens via _wfopen on Windows and passes
through to fopen elsewhere. Route every user-path open through it:

- content: core_fopen (cue/gdi/cdi/track files), the mds/mdf reader, and CHD --
  the latter switched from chd_open(filename) to fopen_utf8 + chd_open_file
  (libchdr's core_file is a plain FILE* and chd_open_file does not take
  ownership, so CHDDisc now owns and closes the handle)
- BIOS and flash (flashrom.h Load/Save)
- VMU, eeprom and the NAOMI fw dump (maple_devs)
- the REIOS font, the Vulkan pipeline cache, and the m3u parser
- the two stat() existence checks in retro_load_game are replaced with
  libretro-common's path_is_directory, which is VFS-routed and UTF-8-safe on
  Windows (path_mkdir already was)

Not covered: files opened inside vendored libzip/lzma by name (NAOMI zip/7z
archives) still use their internal opens; converting those means patching the
vendored libraries and is left for a follow-up if reports warrant it. Debug-only
dumps (modem/pcapng) are unchanged.

Verified: Linux build clean; content in an accented path
(/home/claude/Emulación/Ségà/) loads and runs 400 frames through the new path;
the Windows branch of fopen_utf8 compile+link checked with mingw-w64. Runtime
validation on a real Windows accented path needs a Windows machine.
2026-07-04 06:37:01 +02:00
barbudreadmon 1a0963933f (VULKAN) fix comment 2020-12-31 16:36:09 +01:00
barbudreadmon 900a0fba12 (VULKAN) apiVersion must be the highest version supported by the app 2020-12-31 16:33:50 +01:00
barbudreadmon d545abd144 (VULKAN) amd gpus don't have vkGetPhysicalDeviceFormatProperties2 ? 2020-12-31 10:57:17 +01:00
barbudreadmon 51e97c7eff Revert "Revert "(VULKAN) Vulkan 1.0 support (#999)""
This reverts commit 853adf92f2.
2020-12-31 10:35:16 +01:00
barbudreadmon 853adf92f2 Revert "(VULKAN) Vulkan 1.0 support (#999)"
This reverts commit 1e2f732baa.
2020-12-30 10:18:01 +01:00
bluestang2006 1e2f732baa (VULKAN) Vulkan 1.0 support (#999)
Use gpu's reported vk apiVersion instead of extensions.
2020-12-28 20:04:14 +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 82bb310f86 vulkan: workaround for Intel driver bug when depth write is disabled
https://forums.intel.com/s/question/0D50P00004Xwlb0SAB/vulkan-depth-test-fails-when-depth-buffer-write-is-disabled-and-glfragdepth-is-updated-in-fragment-shader
2019-12-22 18:25:26 +01:00
Flyinghead 399d7305c5 vulkan: disable translucent modifier volumes on adreno
causes all opaque geometry to be black and other glitches
2019-12-16 17:30:31 +01:00
Flyinghead 605e2128cb Add anisotropic filtering option (vulkan). Remove RTT option. 2019-12-16 15:48:33 +01:00
Flyinghead 1180c7ab6f vulkan: use frontend-specified physical device if not nil
might help issue #736
2019-12-13 18:26:45 +01:00
Flyinghead baeb737624 vulkan: arm32 build fix 2019-12-11 20:53:02 +01:00
Flyinghead 485b85682a vulkan: enable on android. fix crash when recreating context
Delete renderer when destroying context
if vkGetPhysicalDeviceFormatProperties2 is null, use
vkGetPhysicalDeviceFormatProperties instead
2019-12-11 15:16:28 +01:00
Flyinghead b0322ba478 vulkan: use libretro nego interface to enable fragmentStoresAndAtomics
Also force vulkan 1.1 for now
2019-12-10 22:13:48 +01:00
Flyinghead bc61520174 vulkan: delay destroying in-flight images
Keep in-use imave and image views when the texture is updated.
Wait until the graphics queue is idle before changing shared buffers and
attachments.
Refactoring
2019-12-09 21:16:16 +01:00
Flyinghead 66f723acef vulkan: allocate oit buffers in device-local mem. NVidia fixes
BufferData was always allocated in host visible mem, regardless of the
mem property argument.
NVidia gpus don't support blitting to 565 and 5551, which is needed for
mipmaps, so convert to 8888 instead.
Use optimal tiling for oit pointers image (linear not supported by
nvidia and probably slower anyway)
oit screen drawer uses multiple final attachments
2019-12-04 15:14:18 +01:00
Flyinghead 4592a287eb vulkan renderer 2019-12-03 17:38:51 +01:00