This commit is contained in:
elwrcl committed 2026-06-06 13:25:15 +03:00
1 parent 04d4dc8b09
commit 818cab85dc
12511 files changed
+6156641

No files matched your search

+173
View File
@@ -0,0 +1,173 @@
:orphan:
.. _aco-fn-calls:
Function call support in RADV/ACO
=================================
ACO supports function calls inside shaders - given a function signature and ABI, shaders can call
an arbitrary function, even via only a function pointer (i.e. with an unknown function definition).
This function call support is useful for implementing ray tracing pipelines (by representing individual RT shaders
as callable functions), but it also has potential use cases in GPGPU/Compute workloads.
This page serves to document the concepts involved in implementing function calls as well as an overview of the
implementation components.
Function call representation
----------------------------
In NIR, function calls are represented by a `nir_call_instr`. The instruction takes a `nir_function` representing
the function being called, as well as SSA defs for each call parameter.
NIR can also represent "indirect calls", i.e. calls where the function being called is
unknown - instead, the instruction takes an SSA def containing a function pointer to the callee. In this case, the
`nir_function` only serves to provide information about the function signature, i.e. how many and which parameters
the function takes.
Call instructions do not have return values - instead, return values are represented by so-called "return parameters".
Instead of an SSA value, these parameters are derefs, and the return value is written into the deref when the callee
returns. Return parameters can double as input parameters, too - the callee can read the previous value of the deref
before (potentially) overwriting it with a new value.
ACO's representation of function calls follows this very closely. Calls are described by the `p_call` pseudo-instruction.
The operands to this instruction are a function pointer (i.e. the address of the callee), followed by the call
parameters. Return parameters are handled differently, though: While the initial value of the return parameter is passed
as an operand, the call instruction produces new definitions that refer to the SSA values of the return parameters after
the function call returns. There is a special NIR intrinsic ``load_return_param_amd`` that can be used to access these
new definitions when lowering return parameter derefs to SSA form.
.. _div-calls:
Divergent calls
---------------
On CPUs, a call instruction will only ever jump to a single address. However, GPUs are SIMT, and the value of a function
pointer may be divergent, i.e. different threads try calling different functions within the same call instruction. AMD
hardware executes one instruction for all threads in lockstep, so the multiple callees have to be executed one after
the other.
This is handled by RADV in ``radv_nir_lower_call_abi``. In addition to the (non-divergent) function pointer to jump to,
``radv_nir_lower_call_abi`` prepends another parameter representing the (potentially divergent) function pointer for all
lanes. For callable functions, ``radv_nir_lower_call_abi`` wraps the function body in a condition that verifies that the
current thread's (divergent) pointer matches the (non-divergent) pointer that is currently being executed. This serves
to "mask off" all threads that wanted to jump to a different function than what is currently executing. At the very end,
``radv_nir_lower_call_abi`` inserts some code deciding whether to jump to the next callee or to return.
.. _stack:
Stack
-----
Supporting arbitrary function calls also means supporting recursion, and recursive functions need a stack.
AMD hardware provides instructions for accessing a per-thread scratch memory area in VRAM, and ACO uses this per-thread
scratch memory to set up its stack.
The stack frame for a function consists of all scratch memory allocated for this function in NIR, as well as space to
spill VGPRs if that is required. ACO adds a stack pointer as a parameter to every function - this stack pointer is added
to the offset inside the scratch space for all scratch loads/stores to make sure they don't overwrite stack frames of
caller functions.
ACO's call instructions take two stack-related operands: The current (caller) stack pointer and the caller's stack size.
When converting the call instruction to hardware instructions, ACO will add the caller stack size to the stack pointer
for the duration of the call (and subtract it again afterwards). This allows us to re-use the same stack pointer after
the call.
Implicit/System Parameters
--------------------------
In addition to parameters defined by the function signature, both RADV and ACO will insert additional parameters while
lowering calls. This is an overview of which lowering passes add which parameters.
Parameters added by ``radv_nir_lower_call_abi`` (see :ref:`Divergent calls <div-calls>`):
- "Uniform"/Non-divergent callee pointer
- Divergent function pointer
Parameters added by ACO: (see :ref:`Stack <stack>`)
- Stack pointer (uniform)
ABI Definition
--------------
The ABI (Application Binary Interface) defines specifics about the interaction between the function caller and the
callee (e.g. assignment of registers to parameters or register preservation). In ACO, the primary purpose of the ABI is
to define which register ranges are "preserved" (i.e. never overwritten by the callee) or "clobbered" (i.e. potentially
overwritten by the callee).
The caller can use preserved register ranges to store temporaries that are live across a call, and the callee can use
clobbered register ranges to store its own temporaries. If the callee wants to use registers from a preserved range,
then it needs to back up the value contained in the preserved register beforehand, and restore it when it's done using
the preserved register. Similarly, if there are not enough preserved registers for the caller to store all its
temporaries, the caller will need to spill excess temporaries to the stack.
ACO has to cater to different needs when defining ABIs: On one side, ray tracing traversal shaders demand to free up
the entire register file for the callee (Ray traversal is a really hot loop, so we don't want to spill anything at all).
Besides some parameters like the invocation ID, these shaders should be able to overwrite almost anything. On the other
side, RT traversal shaders should not be required to free up the register file when calling any-hit/intersection shaders
as this would also cause spilling during traversal. GPGPU compute workloads could fall anywhere between these extremes,
so a middle-ground solution is desirable for these.
ACO's way of defining an ABI divides the register file into "blocks" (``struct aco::ABI::RegisterBlock``). Each block
consists of a fixed number of preserved and clobbered registers, and a boolean determining whether the preserved or
clobbered registers come first in the block. Preserved and clobbered register ranges are defined by
repeating these blocks for as long as there are unassigned registers.
Some examples of preserved/clobbered register ranges using this approach::
For all examples, there are 108 SGPRs and 128 VGPRs to assign.
RegisterBlock:
clobbered_size: {16 sgpr, 16 vgpr}
preserved_size: {16 sgpr, 16 vgpr}
clobbered_first: false
results in:
v0-v15: preserved
v16-v31: clobbered
v32-v47: preserved
v48-v63: clobbered
v64-v79: preserved
v80-v95: clobbered
v96-v111: preserved
v112-v127: clobbered
s0-s15: preserved
s16-s31: clobbered
s32-s47: preserved
s48-s63: clobbered
s64-s79: preserved
s80-s95: clobbered
s96-s108: preserved
RegisterBlock:
clobbered_size: {128 sgpr, 256 vgpr}
preserved_size: {80 sgpr, 80 vgpr}
clobbered_first: false
results in:
v0-v79: preserved
v80-v127: clobbered
s0-s79: preserved
s80-s108: clobbered
An alternating preserved-clobbered-preserved pattern is useful for generic compute workloads, because the ratio of
preserved to clobbered registers is roughly the same, no matter how many registers are used by the shaders.
The latter example where the lower part of the register file is preserved and only some registers high up in the
register file are clobbered is suitable for any-hit/intersection shaders - traversal shader temporaries can live in the
preserved part low in the register file.
This block assignment is optional - if no ``RegisterBlock`` is given, the ABI defines the entire register range as
clobbered-by-default, although parameters that are not marked as clobbered via ``ACO_NIR_PARAM_ATTRIB_DISCARDABLE``
will continue being preserved.
Parameter Register Assignment
-----------------------------
If a ``RegisterBlock`` defines preserved and clobbered ranges, then parameters are assigned registers from either range
depending on ``ACO_NIR_PARAM_ATTRIB_DISCARDABLE`` - if parameters are marked as clobbered with this attribute, then they
are assigned a register in a clobbered range, otherwise they are assigned in a register in a preserved range. The order
of the parameters in the register file is not necessarily the same order as in the function signature - they may get
reordered if it's beneficial to fill gaps or for alignment.
If there is no ``RegisterBlock``, then registers will be assigned based on alignment only.
If there is no more space for a parameter in any of its corresponding register ranges, it will be moved to the stack.
+85
View File
@@ -0,0 +1,85 @@
:orphan:
.. _radv-debug-hang:
Debugging GPU hangs with RADV
=============================
UMR (optional)
--------------
UMR is needed for dumping a lot of useful information. Clone, build and install
`UMR <https://gitlab.freedesktop.org/tomstdenis/umr>`__. Do not forget to run
``chmod +s $(which umr)`` so RADV can actually access UMR.
UMR needs to access some kernel debug interfaces:
.. code-block:: sh
chmod 777 /sys/kernel/debug
chmod -R 777 /sys/kernel/debug/dri
Secure boot has to be disabled as well.
Generating and analyzing hang reports
-------------------------------------
With UMR installed, you can now set ``RADV_DEBUG=hang`` which makes RADV insert
trace markers and synchronization and check for hangs. The hang report will be
saved to ``~/radv_dumps_<pid>_<time>``. Inside the directory of the hang report,
there are a couple of files:
* ``*.spv``: SPIR-V binaries of the pipeline that was bound when the hang
occurred.
* ``addr_binding_report.log``: VK_EXT_address_binding_report logs.
* ``app_info.log``: ``VkApplicationInfo`` fields.
* ``bo_history.log``: A list of every GPU memory allocation and deallocation.
If the GPU hang was caused by a page fault, you can use
`radv_check_va.py <https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/tools/radv_check_va.py>`__
to figure out if address is invalid or used after the memory was deallocated.
* ``bo_ranges.log``: Address ranges that were valid at the time of submission.
* ``dmesg.log``: Output of ``dmesg``, if available.
* ``gpu_info.log``: Fields of ``radeon_info``.
* ``pipeline.log``: IR of the shaders that were bound during the hang as well as
programm counters of waves executing said shaders and bound descriptors.
* ``registers.log``: Various GPU state registers.
* ``trace.log``: An annotated list of the command stream that caused the hang.
the commands that hung come after
``!!!!! This is the last trace point that was reached by the CP !!!!!``.
* ``umr_ring.log``: Similar to ``trace.log``.
* ``umr_waves.log``: A list of waves that were active at the time of the hang,
including register values.
* ``vm_fault.log``: The page fault address if a page fault occurred.
Note: By default, the backend IR (ACO or LLVM) and the disassembly should be
dumped to ``pipeline.log``. But due to shaders caching, you might need
``RADV_DEBUG=hang,nocache`` to get SPIR-V and NIR in the GPU hang report.
Debugging Steam games
---------------------
Steam games require a bit more work so RADV can access UMR: In your Steam library,
make sure **Tools** is checked and search for **Steam Linux Runtime**.
Under **Properties** -> **Installed Files**, click **Browse**, open
``_v2-entry-point`` and add
.. code-block:: sh
shift 2
exec "$@"
at the top of the file. Hang debugging can be enabled by selecting the faulting
game and adding ``RADV_DEBUG=hang %command%`` under **Properties** -> **General**
-> **LAUNCH OPTIONS**.
Debugging hangs without RADV_DEBUG=hang
---------------------------------------
In some situations, ``RADV_DEBUG=hang`` wouldn't be able to generate a GPU hang
report, like for synchronization issues (because it enables
``RADV_DEBUG=syncshaders`` behind the scene). An alternative solution is to
disable GPU recovery by adding ``amdgpu.gpu_recovery=0`` to your kernel command
line options. And then invoke UMR manually with
``umr --by-pci <pci_id> -O bits,halt_waves -go 0 -wa <ring> -go 1 2>&1`` for
dumping the waves and ``umr --by-pci <pci_id> -RS <ring> 2>&1`` for dumping the
rings once the GPU hang occurred.
+477
View File
@@ -0,0 +1,477 @@
Primitive Ordered Pixel Shading
===============================
Primitive Ordered Pixel Shading (POPS) is the feature available starting from
GFX9 that provides the Fragment Shader Interlock or Fragment Shader Ordering
functionality.
It allows a part of a fragment shader — an ordered section (or a critical
section) — to be executed sequentially in rasterization order for different
invocations covering the same pixel position.
This article describes how POPS is set up in shader code and the registers. The
information here is currently provided for architecture generations up to GFX11.
Note that the information in this article is **not official** and may contain
inaccuracies, as well as incomplete or incorrect assumptions. It is based on the
shader code output of the Radeon GPU Analyzer for Rasterizer Ordered View usage
in Direct3D shaders, AMD's Platform Abstraction Library (PAL), ISA references,
and experimentation with the hardware.
Shader code
-----------
With POPS, a wave can dynamically execute up to one ordered section. It is fine
for a wave not to enter an ordered section at all if it doesn't need ordering on
its execution path, however.
The setup of the ordered section consists of three parts:
1. Entering the ordered section in the current wave — awaiting the completion of
ordered sections in overlapped waves.
2. Resolving overlap within the current wave — intrawave collisions (optional
and GFX9–10.3 only).
3. Exiting the ordered section — resuming overlapping waves trying to enter
their ordered sections.
GFX9–10.3: Entering the ordered section in the wave
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Awaiting the completion of ordered sections in overlapped waves is performed by
setting the POPS packer hardware register, and then polling the volatile
``pops_exiting_wave_id`` ALU operand source until its value exceeds the newest
overlapped wave ID for the current wave.
The information needed for the wave to perform the waiting is provided to it via
the SGPR argument ``COLLISION_WAVEID``. Its loading needs to be enabled in the
``SPI_SHADER_PGM_RSRC2_PS`` and ``PA_SC_SHADER_CONTROL`` registers (note that
the POPS arguments specifically need to be enabled not only in ``RSRC`` unlike
various other arguments, but in ``PA_SC_SHADER_CONTROL`` as well).
The collision wave ID argument contains the following unsigned values:
* [31]: Whether overlap has occurred.
* [29:28] (GFX10+) / [28] (GFX9): ID of the packer the wave should be associated
with.
* [25:16]: Newest overlapped wave ID.
* [9:0]: Current wave ID.
The 2020 RDNA and RDNA 2 ISA references contain incorrect offsets and widths of
the fields, possibly from an early development iteration, but the meanings of
them are accurate there.
The wait must not be performed if the "did overlap" bit 31 is set to 0,
otherwise it will result in a hang. Also, the bit being set to 0 indicates that
there are *both* no wave overlap *and no intrawave collisions* for the current
wave — so if the bit is 0, it's safe for the wave to skip all of the POPS logic
completely and execute the contents of the ordered section simply as usual with
unordered access as a potential additional optimization. The packer hardware
register, however, may be set even without overlap safely — it's the wait loop
itself that must not be executed if it was reported that there was no overlap.
The packer ID needs to be passed to the packer hardware register using
``s_setreg_b32`` so the wave can poll ``pops_exiting_wave_id`` on that packer.
On GFX9, the ``MODE`` (1) hardware register has two bits specifying which packer
the wave is associated with:
* [25]: The wave is associated with packer 1.
* [24]: The wave is associated with packer 0.
Initially, both of these bits are set 0, meaning that POPS is disabled for the
wave. If the wave needs to enter the ordered section, it must set bit 24 to 1 if
the packer ID in ``COLLISION_WAVEID`` is 0, or set bit 25 to 1 if the packer ID
is 1.
Starting from GFX10, the ``POPS_PACKER`` (25) hardware register is used instead,
containing the following fields:
* [2:1]: Packer ID.
* [0]: POPS enabled for the wave.
Initially, POPS is disabled for a wave. To start entering the ordered section,
bits 2:1 must be set to the packer ID from ``COLLISION_WAVEID``, and bit 0 needs
to be set to 1.
The wave IDs, both in ``COLLISION_WAVEID`` and ``pops_exiting_wave_id``, are
10-bit values wrapping around on overflow — consecutive waves are numbered 1022,
1023, 0, 1… This wraparound needs to be taken into account when comparing the
exiting wave ID and the newest overlapped wave ID.
Specifically, until the current wave exits the ordered section, its ID can't be
smaller than the newest overlapped wave ID or the exiting wave ID. So
``current_wave_id + 1`` can be subtracted from 10-bit wave IDs to remap them to
monotonically increasing unsigned values. In this case, the largest value,
0xFFFFFFFF, will correspond to the current wave, 10-bit values up to the current
wave ID will be in a range near 0xFFFFFFFF growing towards it, and wave IDs from
before the last wraparound will be near 0 increasing away from it. Subtracting
``current_wave_id + 1`` is equivalent to adding ``~current_wave_id``.
GFX9 has an off-by-one error in the newest overlapped wave ID: if the 10-bit
newest overlapped wave ID is greater than the 10-bit current wave ID (meaning
that it's behind the last wraparound point), 1 needs to be added to the newest
overlapped wave ID before using it in the comparison. This was corrected in
GFX10.
The exiting wave ID (not to be confused with "exited" — the exiting wave ID is
the wave that will exit the ordered section next) is queried via the
``pops_exiting_wave_id`` ALU operand source, numbered 239. Normally, it will be
one of the arguments of ``s_add_i32`` that remaps it from a wrapping 10-bit wave
ID to monotonically increasing one.
It's a volatile operand, and it needs to be read in a loop until its value
becomes greater than the newest overlapped wave ID (after remapping both to
monotonic). However, if it's too early for the current wave to enter the ordered
section, it needs to yield execution to other waves that may potentially be
overlapped — via ``s_sleep``. GFX9 requires a finite amount of delay to be
specified, AMD uses 3. Starting from GFX10, exiting the ordered section wakes up
the waiting waves, so the maximum delay of 0xFFFF can be used.
In pseudocode, the entering logic would look like this::
bool did_overlap = collision_wave_id[31];
if (did_overlap) {
if (gfx_level >= GFX10) {
uint packer_id = collision_wave_id[29:28];
s_setreg_b32(HW_REG_POPS_PACKER[2:0], 1 | (packer_id << 1));
} else {
uint packer_id = collision_wave_id[28];
s_setreg_b32(HW_REG_MODE[25:24], packer_id ? 0b10 : 0b01);
}
uint current_10bit_wave_id = collision_wave_id[9:0];
// Or -(current_10bit_wave_id + 1).
uint wave_id_remap_offset = ~current_10bit_wave_id;
uint newest_overlapped_10bit_wave_id = collision_wave_id[25:16];
if (gfx_level < GFX10 &&
newest_overlapped_10bit_wave_id > current_10bit_wave_id) {
++newest_overlapped_10bit_wave_id;
}
uint newest_overlapped_wave_id =
newest_overlapped_10bit_wave_id + wave_id_remap_offset;
while (!(src_pops_exiting_wave_id + wave_id_remap_offset >
newest_overlapped_wave_id)) {
s_sleep(gfx_level >= GFX10 ? 0xFFFF : 3);
}
}
The SPIR-V fragment shader interlock specification requires an invocation — an
individual invocation, not the whole subgroup — to execute
``OpBeginInvocationInterlockEXT`` exactly once. However, if there are multiple
begin instructions, or even multiple begin/end pairs, under divergent
conditions, a wave may end up waiting for the overlapped waves multiple times.
Thankfully, it's safe to set the POPS packer hardware register to the same
value, or to run the wait loop, multiple times during the wave's execution, as
long as the ordered section isn't exited in between by the wave.
GFX11: Entering the ordered section in the wave
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Instead of exposing wave IDs to shaders, GFX11 uses the "export ready" wave
status flag to report that the wave may enter the ordered section. It's awaited
by the ``s_wait_event`` instruction, with the bit 0 ("don't wait for
``export_ready``") of the immediate operand set to 0. On GFX11 specifically, AMD
passes 0 as the whole immediate operand.
The "export ready" wait can be done multiple times safely.
GFX9–10.3: Resolving intrawave collisions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
On GFX9–10.3, it's possible for overlapping fragment shader invocations to be
placed not only in different waves, but also in the same wave, with the shader
code making sure that the ordered section is executed for overlapping
invocations in order.
This functionality is optional — it can be activated by enabling loading of the
``INTRAWAVE_COLLISION`` SGPR argument in ``SPI_SHADER_PGM_RSRC2_PS`` and
``PA_SC_SHADER_CONTROL``.
The lower 8 or 16 (depending on the wave size) bits of ``INTRAWAVE_COLLISION``
contain the mask of whether each quad in the wave starts a new layer of
overlapping invocations, and thus the ordered section code for them needs to be
executed after running it for all lanes with indices preceding that quad index
multiplied by 4. The rest of the bits in the argument need to be ignored — AMD
explicitly masks them out in shader code (although this is not necessary if the
shader uses "find first 1" to obtain the start of the next set of overlapping
quads or expands this quad mask into a lane mask).
For example, if the intrawave collision mask is 0b0000001110000100, or
``(1 << 2) | (1 << 7) | (1 << 8) | (1 << 9)``, the code of the ordered section
needs to be executed first only for quads 1:0 (lanes 7:0), then only for quads
6:2 (lanes 27:8), then for quad 7 (lanes 31:28), then for quad 8 (lanes 35:32),
and then for the remaining quads 15:9 (lanes 63:36).
This effectively causes the ordered section to be executed as smaller
"sub-subgroups" within the original subgroup.
However, this is not always compatible with the execution model of SPIR-V or
GLSL fragment shaders, so enabling intrawave collisions and wrapping a part of
the shader in a loop may be unsafe in some cases. One particular example is when
the shader uses subgroup operations influenced by lanes outside the current
quad. In this case, the code outside and inside the ordered section may be
executed with different sets of active invocations, affecting the results of
subgroup operations. But in SPIR-V and GLSL, fragment shader interlock is not
supposed to modify the set of active invocations in any way. So the intrawave
collision loop may break the results of subgroup operations in unpredictable
ways, even outside the driver's compiler infrastructure. Even if the driver
splits the subgroup exactly at ``OpBeginInvocationInterlockEXT`` and makes the
lane subsets rejoin exactly at ``OpEndInvocationInterlockEXT``, the application
and the compilers that created the source shader are still not aware of that
happening — the input SPIR-V or GLSL shader might have already gone through
various optimizations, such as common subexpression elimination which might
have considered a subgroup operation before ``OpBeginInvocationInterlockEXT``
and one after it equivalent.
The idea behind reporting intrawave collisions to shaders is to reduce the
impact on the parallelism of the part of the shader that doesn't depend on the
ordering, to avoid wasting lanes in the wave and to allow the code outside the
ordered section in different invocations to run in parallel lanes as usual. This
may be especially helpful if the ordered section is small compared to the rest
of the shader — for instance, a custom blending equation in the end of the usual
fragment shader for a surface in the world.
However, whether handling intrawave collisions is preferred is not a question
with one universal answer. Intrawave collisions are pretty uncommon without
multisampling, or when using sample interlock with multisampling, although
they're highly frequent with pixel interlock with multisampling, when adjacent
primitives cover the same pixels along the shared edge (though that's an
extremely expensive situation in general). But resolving intrawave collisions
adds some overhead costs to the shader. If intrawave overlap is unlikely to
happen often, or even more importantly, if the majority of the shader is inside
the ordered section, handling it in the shader may cause more harm than good.
GFX11 removes this concept entirely, instead overlapping invocations are always
placed in different waves.
GFX9–10.3: Exiting the ordered section in the wave
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To exit the ordered section and let overlapping waves resume execution and enter
their ordered sections, the wave needs to send the ``ORDERED_PS_DONE`` message
(7) using ``s_sendmsg``.
If the wave has enabled POPS by setting the packer hardware register, it *must
not* execute ``s_endpgm`` without having sent ``ORDERED_PS_DONE`` once, so the
message must be sent on all execution paths after the packer register setup.
However, if the wave exits before having configured the packer register, sending
the message is not required, though it's still fine to send it regardless of
that.
Note that if the shader has multiple ``OpEndInvocationInterlockEXT``
instructions executed in the same wave (depending on a divergent condition, for
example), it must still be ensured that ``ORDERED_PS_DONE`` is sent by the wave
only once, and especially not before any awaiting of overlapped waves.
Before the message is sent, all counters for memory accesses that need to be
primitive-ordered, both writes and (in case something after the ordered section
depends on the per-pixel data, for instance, the tail blending fallback in
order-independent transparency) reads, must be awaited. Those may include
``vm``, ``vs``, and in some cases ``lgkm`` (though normally primitive-ordered
memory accesses will be done through VMEM with divergent addresses, not SMEM, as
there's no synchronization between fragments at different pixel coordinates, but
it's still technically possible for a shader, even though pointless and
nonoptimal, to explicitly perform them in a waterfall loop, for instance, and
that must work correctly too). Without that, a race condition will occur when
the newly resumed waves start accessing the memory locations to which there
still are outstanding accesses in the current wave.
Another option for exiting is the ``s_endpgm_ordered_ps_done`` instruction,
which combines waiting for all the counters, sending the ``ORDERED_PS_DONE``
message, and ending the program. Generally, however, it's desirable to resume
overlapping waves as early as possible, including before the export, as it may
stall the wave for some time too.
GFX11: Exiting the ordered section in the wave
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The overlapping waves are resumed when the wave performs the last export (with
the ``done`` flag).
The same requirements for awaiting the memory access counters as on GFX9–10.3
still apply.
Memory access requirements
^^^^^^^^^^^^^^^^^^^^^^^^^^
The compiler needs to ensure that entering the ordered section implements
acquire semantics, and exiting it implements release semantics, in the fragment
interlock memory scope for ``UniformMemory`` and ``ImageMemory`` SPIR-V storage
classes.
A fragment interlock memory scope instance includes overlapping fragment shader
invocations executed by commands inside a single subpass. It may be considered a
subset of a queue family memory scope instance from the perspective of memory
barriers.
Fragment shader interlock doesn't perform implicit memory availability or
visibility operations. Shaders must do them by themselves for accesses requiring
primitive ordering, such as via ``coherent`` (``queuefamilycoherent``) in GLSL
or ``MakeAvailable`` and ``MakeVisible`` in at least the ``QueueFamily`` scope
in SPIR-V.
On AMD hardware, this means that the accessed memory locations must be made
available or visible between waves that may be executed on any compute unit — so
accesses must go directly to the global L2 cache, bypassing L0$ via the GLC flag
and L1$ via DLC.
However, it should be noted that memory accesses in the ordered section may be
expected by the application to be done in primitive order even if they don't
have the GLC and DLC flags. Coherent access not only bypasses, but also
invalidates the lower-level caches for the accessed memory locations. Thus,
considering that normally per-pixel data is accessed exclusively by the
invocation executing the ordered section, it's not necessary to make all reads
or writes in the ordered section for one memory location to be GLC/DLC — just
the first read and the last write: it doesn't matter if per-pixel data is cached
in L0/L1 in the middle of a dependency chain in the ordered section, as long as
it's invalidated in them in the beginning and flushed to L2 in the end.
Therefore, optimizations in the compiler must not simply assume that only
coherent accesses need primitive ordering — and moreover, the compiler must also
take into account that the same data may be accessed through different bindings.
Export requirements
^^^^^^^^^^^^^^^^^^^
With POPS, on all hardware generations, the shader must have at least one
export, though it can be a null or an ``off, off, off, off`` one.
Also, even if the shader doesn't need to export any real data, the export
skipping that was added in GFX10 must not be used, and some space must be
allocated in the export buffer, such as by setting ``SPI_SHADER_COL_FORMAT`` for
some color output to ``SPI_SHADER_32_R``.
Without this, the shader will be executed without the needed synchronization on
GFX10, and will hang on GFX11.
Drawing context setup
---------------------
Configuring POPS
^^^^^^^^^^^^^^^^
Most of the configuration is performed via the ``DB_SHADER_CONTROL`` register.
To enable POPS for the draw,
``DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER`` should be set to 1.
On GFX9–10.3, ``DB_SHADER_CONTROL.POPS_OVERLAP_NUM_SAMPLES`` controls which
fragment shader invocations are considered overlapping:
* For pixel interlock, it must be set to 0 (1 sample).
* If sample interlock is sufficient (only synchronizing between invocations that
have any common sample mask bits), it may be set to
``PA_SC_AA_CONFIG.MSAA_EXPOSED_SAMPLES`` — the number of sample coverage mask
bits passed to the shader which is expected to use the sample mask to
determine whether it's allowed to access the data for each of the samples. As
of April 2023, PAL for some reason doesn't use non-1x
``POPS_OVERLAP_NUM_SAMPLES`` at all, even when using Direct3D Rasterizer
Ordered Views or ``GL_INTEL_fragment_shader_ordering`` with sample shading
(those APIs tie the interlock granularity to the shading frequency — Vulkan
and OpenGL fragment shader interlock, however, allows specifying the interlock
granularity independently of it, making it possible both to ask for finer
synchronization guarantees and to require stronger ones than Direct3D ROVs can
provide). However, with MSAA, on AMD hardware, pixel interlock generally
performs *massively*, sometimes prohibitively, slower than sample interlock,
because it causes fragment shader invocations along the common edge of
adjacent primitives to be ordered as they cover the same pixels (even though
they don't cover any common samples). So it's highly desirable for the driver
to provide sample interlock, and to set ``POPS_OVERLAP_NUM_SAMPLES``
accordingly, if the shader declares that it's enough for it via the execution
mode.
On GFX11, when POPS is enabled, ``DB_SHADER_CONTROL.OVERRIDE_INTRINSIC_RATE`` is
used in place of ``DB_SHADER_CONTROL.POPS_OVERLAP_NUM_SAMPLES`` from the earlier
architecture generations (and has a different bit offset in the register), and
``DB_SHADER_CONTROL.OVERRIDE_INTRINSIC_RATE_ENABLE`` must be set to 1. The GFX11
blending performance workaround overriding the intrinsic rate must not be
applied if POPS is used in the draw — the intrinsic rate override must be used
solely to control the interlock granularity in this case.
No explicit flushes/synchronization are needed when changing the pipeline state
variables that may be involved in POPS, such as the rasterization sample count.
POPS automatically keeps synchronizing invocations even between draws with
different sample counts (invocations with common coverage mask bits are
considered overlapping by the hardware, regardless of what those samples
actually are — only the indices are important).
Also, on GFX11, POPS uses ``DB_Z_INFO.NUM_SAMPLES`` to determine the coverage
sample count, and it must be equal to ``PA_SC_AA_CONFIG.MSAA_EXPOSED_SAMPLES``
even if there's no depth/stencil target.
Hardware bug workarounds
^^^^^^^^^^^^^^^^^^^^^^^^
Early revisions of GFX9 — ``CHIP_VEGA10`` and ``CHIP_RAVEN`` — contain a
hardware bug that may result in a hang, and need a workaround to be enabled.
Specifically, if POPS is used with 8 or more rasterization samples, or with 8 or
more depth/stencil target samples, ``DB_DFSM_CONTROL.POPS_DRAIN_PS_ON_OVERLAP``
must be set to 1 for draws that satisfy this condition. In PAL, this is the
``waMiscPopsMissedOverlap`` workaround. It results in slightly lower performance
in those cases, increasing the frame time by around 1.5 to 2 times in
`nvpro-samples/vk_order_independent_transparency <https://github.com/nvpro-samples/vk_order_independent_transparency>`_
on the RX Vega 10, but it's required in a pretty rare case (8x+ MSAA) and is
mandatory to ensure stability.
Also, even though ``DB_DFSM_CONTROL.POPS_DRAIN_PS_ON_OVERLAP`` is not required
on chips other than the ``CHIP_VEGA10`` and ``CHIP_RAVEN`` GFX9 revisions, if
it's enabled for some reason on GFX10.1 (``CHIP_NAVI10``, ``CHIP_NAVI12``,
``CHIP_NAVI14``), and the draw uses POPS,
``DB_RENDER_OVERRIDE2.PARTIAL_SQUAD_LAUNCH_CONTROL`` must be set to
``PSLC_ON_HANG_ONLY`` to avoid a hang (see ``waStalledPopsMode`` in PAL).
Out-of-order rasterization interaction
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This is a largely unresearched topic currently. However, considering that POPS
is primarily the functionality of the Depth Block, similarity to the behavior of
out-of-order rasterization in depth/stencil testing may possibly be expected.
If the shader specifies an ordered interlock execution mode, out-of-order
rasterization likely must not be enabled implicitly.
As of April 2023, PAL doesn't have any rules specifically for POPS in the logic
determining whether out-of-order rasterization can be enabled automatically.
Some of the POPS usage cases may possibly be covered by the rule that always
disables out-of-order rasterization if the shader writes to Unordered Access
Views (storage resources), though fragment shader interlock can be used for
read-only purposes too (for ordering between draws that only read per-pixel data
and draws that may write it), so that may be an oversight.
Explicitly enabled relaxed rasterization order modifies the concept of
rasterization order itself in Vulkan, so from the point of view of the
specification of fragment shader interlock, relaxed rasterization order should
still be applicable regardless of whether the shader requests ordered interlock.
PAL also doesn't make any POPS-specific exceptions here as of April 2023.
Variable-rate shading interaction
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
On GFX10.3, enabling ``DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER`` forces
the shading rate to be 1x1, thus the
``fragmentShadingRateWithFragmentShaderInterlock`` Vulkan device property must
be false.
On GFX11, by default, POPS itself can work with non-1x1 shading rates, and the
``fragmentShadingRateWithFragmentShaderInterlock`` property must be true.
However, if ``PA_SC_VRS_SURFACE_CNTL_1.FORCE_SC_VRS_RATE_FINE_POPS`` is set,
enabling POPS will force 1x1 shading rate.
The widest interlock granularity available on GFX11 — with the lowest possible
Depth Block intrinsic rate, 1x — is per-fine-pixel, however. There's no
synchronization between coarse fragment shader invocations if they don't cover
common fine pixels, so the ``fragmentShaderShadingRateInterlock`` Vulkan device
feature is not available.
Additional configuration
^^^^^^^^^^^^^^^^^^^^^^^^
These are some largely unresearched options found in the register declarations.
PAL doesn't use them, so it's unknown if they make any significant difference.
No effect was found in `nvpro-samples/vk_order_independent_transparency <https://github.com/nvpro-samples/vk_order_independent_transparency>`_
during testing on GFX9 ``CHIP_RAVEN`` and GFX11 ``CHIP_NAVI31``.
* ``DB_SHADER_CONTROL.EXEC_IF_OVERLAPPED`` on GFX9–10.3.
* ``PA_SC_BINNER_CNTL_0.BIN_MAPPING_MODE = BIN_MAP_MODE_POPS`` on GFX10-11.5.
This field is reserved on GFX12+ and should be set to 0.
+164
View File
@@ -0,0 +1,164 @@
:orphan:
.. _aco-live-var-analysis:
Description of live variable analysis in ACO
============================================
Operand flags
-------------
Operands can have several different flags which are set by live variable analysis:
* ``isKill``: this operand is not live after the instruction
* ``isFirstKill``: this is the first instance of a temporary in the instruction with ``isKill=true``
* ``isLateKill``: this operand cannot use the same registers as any definition
* ``isClobbered``: this operand will use some of the same registers as a definition
* ``isCopyKill``: this operand must use a different register than earlier instances of the same temporary in the
instruction's operand list
Note that:
* ``isFirstKill=true`` requires that the operand is also ``isKill=true``.
* If ``isKill=true``, then ``isLateKill=true`` requires that the operand is also ``isFirstKill=true`` or
``isCopyKill=true``.
* ``isCopyKill=true`` is incompatible with ``isFirstKill=true`` in the same operand.
* ``isLateKill=true`` cannot be used with ``isClobbered=true`` for the same temporary in an instruction.
* If ``isCopyKill=true``, then the operand will also have ``isKill=true``, even if the temporary is live after the
instruction.
Operands and definitions can be "tied", indicated by ``get_tied_defs()``, meaning that the must use the same registers:
* Operands tied to definitions have ``isClobbered=true``.
* If a clobbered operand has ``isKill=false``, it will be moved to a different register.
* If two operands of the same temporary are tied to different definitions of the same instruction, the second of those
operands will have ``isCopyKill=true``.
There also exists the ``isVectorAligned`` flag. This can be used to define a vector of operands, starting with
``isVectorAligned=true`` and ending with ``isVectorAligned=false``, which must be placed in consecutive registers.
To prevent issues with register allocation, an operand being part of a vector means:
* Unless a vector is tied to a definition, it will also have ``isLateKill=true``, so that a partially killed vectors
are not required to share the same space with definitions.
* If two operands of the same temporary are both part of vectors in the same instruction, the second of those operands
will have ``isCopyKill=true``.
Register demand calculation
---------------------------
``live_in``
temporaries live before the instruction
``live_out``
temporaries live after the instruction
``live_through``
temporaries live both before and after the instruction
``live_definitions``
temporaries defined and used later (definitions where ``isKill=false``)
``dead_definitions``
temporaries defined and not used later (definitions where ``isKill=true``)
``early_kill_operands``
temporaries killed which are not marked late kill (operands where ``isFirstKill=true && isLateKill=false``)
``late_kill_operands``
temporaries killed which are marked late kill (operands where ``isFirstKill=true && isLateKill=true``)
``first_kill_operands``
temporaries killed by the instruction (operands where ``isFirstKill=true``)
``early_kill_operands + late_kill_operands``
``copied_operands``
operands which are either clobbered but not killed, or copy-kill (operands where
``isCopyKill=true || (isClobbered=true && isKill=false)``)
``early_kill_copies``
``copied_operands`` which are not marked late kill (operands where
``(isCopyKill=true && isLateKill=false) || (isClobbered=true && isKill=false)``)
``late_kill_copies``
``copied_operands`` which are marked late kill (operands where ``(isCopyKill=true && isLateKill=true)``)
``live_out``
``live_through + live_definitions``
``live_in - first_kill_operands + live_definitions``
``live_in``
``live_out - live_definitions + first_kill_operands``
``live_through + first_kill_operands``
``live_through``
``live_out - live_definitions``
``live_in - first_kill_operands``
Breakdown of register demand stages using ``live_in``:
* stage 0: before instruction: ``live_in``
* stage 1: setup operands: ``live_in + early_kill_copies + late_kill_copies``
* stage 2: during instruction: ``live_in - early_kill_operands + late_kill_copies``
* stage 3: write definitions: ``live_in - early_kill_operands + late_kill_copies + live_definitions + dead_definitions``
* stage 4: after instruction: ``live_in - early_kill_operands - late_kill_operands + live_definitions``
Breakdown of register demand stages using ``live_through``:
* stage 0: before instruction: ``live_through + late_kill_operands + early_kill_operands``
* stage 1: setup operands: ``live_through + late_kill_operands + early_kill_operands + early_kill_copies + late_kill_copies``
* stage 2: during instruction: ``live_through + late_kill_operands + late_kill_copies``
* stage 3: write definitions: ``live_through + late_kill_operands + late_kill_copies + live_definitions + dead_definitions``
* stage 4: after instruction: ``live_through + live_definitions``
Breakdown of register demand stages using ``live_out``:
* stage 0: before instruction: ``live_out - live_definitions + late_kill_operands + early_kill_operands``
* stage 1: setup operands: ``live_out - live_definitions + late_kill_operands + early_kill_operands + early_kill_copies + late_kill_copies``
* stage 2: during instruction: ``live_out - live_definitions + late_kill_operands + late_kill_copies``
* stage 3: write definitions: ``live_out + dead_definitions + late_kill_operands + late_kill_copies``
* stage 4: after instruction: ``live_out``
If instruction B immediately follows instruction A, then stage 0 of instruction B equals stage 4 of instruction A.
``Instruction::register_demand`` is ``max(stage1, stage3)``, which is equal to the maximum of all stages.
There are a few helper functions for examining how an instruction changes register demand:
``get_live_changes()``
This is the register demand change from killed temporaries and live definitions.
``live_definitions - first_kill_operands``
equal to ``live_out - live_in``
``get_temp_registers()``
This is the temporary increase in register demand needed for copy-kill operands, late-kill operands, clobbered
operands, and dead definitions.
``max(early_kill_operands + late_kill_operands + early_kill_copies + late_kill_copies - live_definitions, late_kill_operands + late_kill_copies + dead_definitions)``
equal to ``register_demand - live_out``
``get_temp_reg_changes()``
Since ``register_demand`` is ``max(stage1, stage3)``, this can be used to know what's the effect of marking an
operand killed will be.
``live_definitions + dead_definitions - early_kill_operands - early_kill_copies``
equal to ``stage3 - stage1``
They can be used as follows:
* ``register_demand(a) = live_in(a) + live_changes(a) + temp_registers(a)``
* ``register_demand(a) = live_out(a) + temp_registers(a)``
Assuming ``stage4(a)==stage0(b)``:
* ``register_demand(a) = register_demand(b) - temp_registers(b) - live_changes(b) + temp_registers(a)``
* ``register_demand(b) = register_demand(a) - temp_registers(a) + live_changes(b) + temp_registers(b)``
(note that ``max(a + b, a + c) - max(b, c) = a`` and ``a + max(b, c) = max(a + b, a + c)``)