Files
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

134 lines
5.7 KiB
C++

/* GPU-side TA processing on Vulkan: a frame's vertices, indices and
* modifier volume triangles made on the GPU from the blocks the game sent
* (rend/ta_gpu.h), and the frame drawn from them, with nothing read back.
*
* - The TA contexts' data are slices of one persistently mapped buffer of
* host-cached memory (tactx_gpu_pool_attach()): the SH4 writes a frame
* straight into memory the GPU reads, and reads its control words back
* from it at the speed of ordinary memory. A slice is written again once
* the work of the frame that read it last is done, which is known by
* polling the texture command pool's fences, never by waiting.
* - At the start of Process() the header pass (ta_gpu_prepare()) writes
* the frame's tables into this frame's piece of a ring, and makes the
* render context's lists of draw groups in the full parse's place; the
* decode is recorded into a command buffer of the texture pool's,
* submitted with the frame before its draws, which are indirect: their
* counts are the GPU's (TaGpuDraw). A frame the header pass does not do
* (ta_gpu_prepare()'s TA_GPU_WHY_*) is parsed and drawn as ever.
* - The kernels (SPIR-V made when the core is built: rend/ta_gpu_spv.inc),
* every buffer (sized for the largest frame the machine has), the
* descriptor sets and the timestamp queries are made in Setup(), when
* the renderer is set up or the option changes, and the pipelines go
* through the context's pipeline cache; nothing is allocated or
* compiled while a game runs.
* - The outputs are on the device, one copy: frames are decoded one after
* the other on the one queue, and each decode waits for whatever the one
* before is read by.
*
* Off (TA processing: CPU), none of this exists. */
#pragma once
#include "vulkan.h"
#include "buffer.h"
#include "hw/pvr/ta.h"
#include <memory>
#include <vector>
class CommandPool;
/* What a frame decoded on the GPU is drawn from (rend_context::gpu): the
* vertex, index and modifier volume triangle buffers; the opaque and
* punch-through draw groups' commands, at their slot (PolyParam::first);
* the translucent draw records, each translucent draw group's at its
* place (first: a count, then at most PolyParam::count draws); and the
* translucent polygon parameters' words of the per-pixel renderer.
* drawCount: vkCmdDrawIndexedIndirectCountKHR(), or null, when a group's
* whole room is drawn (its records past the count draw nothing). */
struct TaGpuDraw
{
vk::Buffer vertices, indices, modtrig, commands, trDraws, trParams;
vk::DeviceSize trParamsSize = 0;
/* the frame's words (TAG_O_*): 1/fZ_max at TAG_O_ZSCALE, which the
* drawers copy into their vertex uniforms (rend/depth_scale.h) */
vk::Buffer frame;
PFN_vkCmdDrawIndexedIndirectCountKHR drawCount = nullptr;
/* The second commands of the opaque and punch-through draw groups for
* shadows (TA_GPU_SHADOWS, TAG_K_SHADOW): a group's at this many bytes
* on from its own */
vk::DeviceSize shadowCommands = 0;
/* (TA_GPU_SORT) this frame's room for the sorted translucent triangles'
* indices (ta_gpu_sorted_write()), mapped */
vk::Buffer sorted;
vk::DeviceSize sortedOffset = 0;
u32 *sortedMap = nullptr;
};
class TaDecoder
{
public:
TaDecoder();
~TaDecoder();
/* Made, or taken down, as the option says (@wanted), for the texture
* pool's size: when the renderer is set up (its context made, or its
* size changed) and when the options change, never while a frame is
* drawn. Off, or where it cannot be made, nothing of it exists. */
void Setup(CommandPool& pool, bool wanted);
/* At the start of each Process(), after the texture pool's
* BeginFrame(); nothing is made here. The GPU's progress is told to
* the TA contexts' pool here. */
void BeginFrame(CommandPool& pool);
void Term();
bool Ready() const { return ready; }
/* The frame's tables, and with TA_GPU_DRAW (in @flags, TA_GPU_*) its
* render context made ready to draw in the full parse's place; whether
* it is decoded, and then (@rv) whether it has something to draw */
bool Prepare(TA_context *ctx, u32 flags, bool *rv);
/* ...and its decode recorded */
void Record(TA_context *ctx, CommandPool& pool);
// what the drawers draw a decoded frame from
const TaGpuDraw *Draw() const { return &draw; }
// whether the translucent draws can number their strips (firstInstance)
bool HasInstance() const { return instance; }
/* The per-triangle renderer's: room for the translucent triangles it
* sorts (TA_GPU_SORT), made with the rest; and whether there is */
void WantSort(bool on) { sortWanted = on; }
bool CanSort() const { return sortable; }
// for measuring
u64 statFrames = 0; // decoded
u64 statSkipped = 0; // left to the CPU
u64 statWhy[TA_GPU_WHY_COUNT] = {};
double statGpuMs = 0; // GPU time of the decodes timed
double statGpuMaxMs = 0;
u64 statGpuTimed = 0;
u64 statSorted = 0; // decoded with a sorted pass, sorted on this side
double statSortUs = 0; // ...what the sorts took
double statSortMaxUs = 0;
u64 statShadowed = 0; // with draw groups the GPU says take shadows or not
private:
bool Init(CommandPool& pool);
void PollDone(CommandPool& pool);
void Log(const char *when);
struct Resources;
std::unique_ptr<Resources> res;
bool ready = false;
bool failed = false; // could not be made on this device: not tried again
bool instance = false;
bool sortWanted = false;
bool sortable = false;
int frames = 0;
int index = 0;
u32 serial = 0; // this frame's
u32 done = 0; // every frame up to this one is done
std::vector<u32> slotSerial; // the frame each slot of the pool last had
std::vector<bool> slotTimed; // ...and whether it has a decode timed
bool prepared = false;
u32 flags = 0;
TaGpuFrame frame;
TaGpuDraw draw;
};