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

+71
View File
@@ -0,0 +1,71 @@
Benchmarking Panfrost
=====================
Obtaining reproducible benchmark timings can sometimes be tricky on Mali
based SoCs. There are a number of things that can be done to make the timings
more predictable.
Make sure the device is actively cooled (to eliminate thermal throttling)
-------------------------------------------------------------------------
This isn't strictly speaking necessary, if you use low enough frequencies
in the steps below, but it is recommended.
Fix the GPU frequency
---------------------
If power management is allowed to vary the GPU frequency, this will lead to
uncertainty in benchmark figures, particularly for shorter benchmark runs.
On most systems, the GPU frequency can be set to a fixed value via a command
like the following (run it as root, sudo alone won't allow the redirection):
.. code-block:: sh
cat /sys/devices/platform/fb000000.gpu/devfreq/fb000000.gpu/min_freq > /sys/devices/platform/fb000000.gpu/devfreq/fb000000.gpu/max_freq
This forces the GPU to always use its minimum frequency. You could reverse
`min_freq` and `max_freq` to fix it to its maximum frequency, if your system
is adequately cooled.
Fix the CPU frequency
---------------------
It is easy to overlook the role the CPU plays in driver performance. To set
its frequency to a fixed value, install the `linux-cpupower` package and
run something like:
.. code-block:: sh
sudo cpupower frequency-info
sudo cpupower frequency-set -d 1.20
sudo cpupower frequency-set -u 1.20
sudo cpupower frequency-info
Adjust the numbers based on what the board actually supports (seen in
the first `frequency-info`. It is best to choose fairly low settings
unless you're confident of the board's cooling.
Pin the benchmark program to a particular core
----------------------------------------------
This is especially important on devices with a big.LITTLE architecture, where
having the benchmark be scheduled on a different kind of core can drastically
change the performance. But even without this, variations in cache residency
and scheduling of kernel tasks can lead to changes in benchmark timing.
Experience shows that using a little core for the benchmark program
tends to give more reproducible results (probably because they have smaller
caches, and also are less likely to be scheduled to by the kernel).
However, forcing the benchmark to run on a little core could change the
bottleneck from GPU to CPU, so watch out for that.
Pinning a task to a particular core is done with the `taskset` command. An
example for the Rock5b (rk3588) board is:
.. code-block:: sh
taskset 0x04 glmark2-es2-wayland
Replace `0x04` with an appropriate mask for your board, and
`glmark2-es2-wayland` with the benchmark program to be used.
+78
View File
@@ -0,0 +1,78 @@
drm-shim
========
Panfrost implements ``drm-shim``, stubbing out the Panfrost kernel interface.
Use cases for this functionality include:
- Future hardware bring up
- Running shader-db on non-Mali workstations
- Reproducing compiler (and some driver) bugs without Mali hardware
Although Mali hardware is usually paired with an Arm CPU, Panfrost is portable C
code and should work on any Linux machine. In particular, you can test the
compiler on shader-db on an Intel desktop.
To build Mesa with Panfrost drm-shim, configure Meson with
``-Dgallium-drivers=panfrost`` and ``-Dtools=drm-shim``. See the above
building section for a full invocation. The drm-shim binary will be built to
``build/src/panfrost/drm-shim/libpanfrost_noop_drm_shim.so``.
To use, set the ``LD_PRELOAD`` environment variable to the drm-shim binary.
By default, drm-shim mocks a Mali-G52 system. To select a specific Mali GPU,
set the ``PAN_GPU_ID`` environment variable to the desired GPU ID:
========= ============= =======
Product Architecture GPU ID
========= ============= =======
Mali-T720 Midgard (v4) 720
Mali-T860 Midgard (v5) 860
Mali-G72 Bifrost (v6) 6221
Mali-G52 Bifrost (v7) 7212
Mali-G57 Valhall (v9) 9093
Mali-G610 Valhall (v10) a867
========= ============= =======
Additional GPU IDs are enumerated in the ``pan_model_list`` list in
``src/panfrost/model/pan_model.c``.
As an example: assuming Mesa is installed to a local path ``~/lib`` and Mesa's
build directory is ``~/mesa/build``, a shader can be compiled for Mali-G52 as:
.. code-block:: sh
~/shader-db$ BIFROST_MESA_DEBUG=shaders \
LD_PRELOAD=~/mesa/build/src/panfrost/drm-shim/libpanfrost_noop_drm_shim.so \
PAN_GPU_ID=7212 \
./run shaders/glmark/1-1.shader_test
The same shader can be compiled for Mali-T720 as:
.. code-block:: sh
~/shader-db$ MIDGARD_MESA_DEBUG=shaders \
LD_PRELOAD=~/mesa/build/src/panfrost/drm-shim/libpanfrost_noop_drm_shim.so \
PAN_GPU_ID=720 \
./run shaders/glmark/1-1.shader_test
These examples set the compilers' ``shaders`` debug flags to dump the optimized
NIR, backend IR after instruction selection, backend IR after register
allocation and scheduling, and a disassembly of the final compiled binary.
As another example, this invocation runs a single dEQP test "on" Mali-G52,
pretty-printing GPU data structures and disassembling all shaders
(``PAN_MESA_DEBUG=trace``) as well as dumping raw GPU memory
(``PAN_MESA_DEBUG=dump``). The ``EGL_PLATFORM=surfaceless`` environment variable
and various flags to dEQP mimic the surfaceless environment that our
continuous integration (CI) uses. This eliminates window system dependencies,
although it requires a specially built CTS:
.. code-block:: sh
~/VK-GL-CTS/build/external/openglcts/modules$ PAN_MESA_DEBUG=trace,dump \
LD_PRELOAD=~/mesa/build/src/panfrost/drm-shim/libpanfrost_noop_drm_shim.so \
PAN_GPU_ID=7212 EGL_PLATFORM=surfaceless \
./glcts --deqp-surface-type=pbuffer \
--deqp-gl-config-name=rgba8888d24s8ms0 --deqp-surface-width=256 \
--deqp-surface-height=256 -n \
dEQP-GLES31.functional.shaders.builtin_functions.common.abs.float_highp_compute
+117
View File
@@ -0,0 +1,117 @@
Instancing
==========
The attribute descriptor lets the attribute unit compute the address of an
attribute given the vertex and instance ID. Unfortunately, the way this works is
rather complicated when instancing is enabled.
To explain this, first we need to explain how compute and vertex threads are
dispatched. When a quad is dispatched, it receives a single, linear index.
However, we need to translate that index into a (vertex id, instance id) pair.
One option would be to do:
.. math::
\text{vertex id} = \text{linear id} \% \text{num vertices}
\text{instance id} = \text{linear id} / \text{num vertices}
but this involves a costly division and modulus by an arbitrary number.
Instead, we could pad ``num_vertices``. We dispatch
:math:`\text{padded_num_vertices} \cdot \text{num_instances}` threads instead
of :math:`\text{num_vertices} \cdot \text{num_instances}`, which results
in some "extra" threads with :math:`\text{vertex_id} \geq \text{num_vertices}`,
which we have to discard. The more we pad ``num_vertices``, the more "wasted"
threads we dispatch, but the division is potentially easier.
One straightforward choice is to pad ``num_vertices`` to the next power
of two, which means that the division and modulus are just simple bit shifts
and masking. But the actual algorithm is a bit more complicated. The thread
dispatcher has special support for dividing by 3, 5, 7, and 9, in addition
to dividing by a power of two. As a result, ``padded_num_vertices`` can
be 1, 3, 5, 7, or 9 times a power of two. This results in less wasted threads,
since we need less padding.
``padded_num_vertices`` is picked by the hardware. The driver just specifies
the actual number of vertices. Note that ``padded_num_vertices`` is a multiple
of four (presumably because threads are dispatched in groups of 4). Also,
``padded_num_vertices`` is always at least one more than ``num_vertices``,
which seems like a quirk of the hardware. For larger ``num_vertices``, the
hardware uses the following algorithm: using the binary representation of
``num_vertices``, we look at the most significant set bit as well as the
following 3 bits. Let n be the number of bits after those 4 bits. Then we
set ``padded_num_vertices`` according to the following table:
========== =======================
high bits ``padded_num_vertices``
========== =======================
1000 :math:`9 \cdot 2^n`
1001 :math:`5 \cdot 2^{n+1}`
101x :math:`3 \cdot 2^{n+2}`
110x :math:`7 \cdot 2^{n+1}`
111x :math:`2^{n+4}`
========== =======================
For example, if :math:`\text{num_vertices} = 70` is passed to
:c:func:`glDraw()`, its binary representation is 1000110, so :math:`n = 3`
and the high bits are 1000, and therefore
:math:`\text{padded_num_vertices} = 9 \cdot 2^3 = 72`.
The attribute unit works in terms of the original ``linear_id``. if
:math:`\text{num_instances} = 1`, then they are the same, and everything
is simple. However, with instancing things get more complicated. There are
four possible modes, two of them we can group together:
1. Use the ``linear_id`` directly. Only used when there is no instancing.
2. Use the ``linear_id`` modulo a constant. This is used for per-vertex
attributes with instancing enabled by making the constant equal
``padded_num_vertices``. Because the modulus is always ``padded_num_vertices``,
this mode only supports a modulus that is a power of 2 times 1, 3, 5, 7,
or 9. The shift field specifies the power of two, while the ``extra_flags``
field specifies the odd number. If :math:`\text{shift} = n` and
:math:`\text{extra_flags} = m`, then the modulus is
:math:`(2m + 1) \cdot 2^n`. As an example, if
:math:`\text{num_vertices} = 70`, then as computed above,
:math:`\text{padded_num_vertices} = 9 \cdot 2^3`, so we should set
:math:`\text{extra_flags} = 4` and :math:`\text{shift} = 3`. Note that we
must exactly follow the hardware algorithm used to get ``padded_num_vertices``
in order to correctly implement per-vertex attributes.
3. Divide the ``linear_id`` by a constant. In order to correctly implement
instance divisors, we have to divide ``linear_id`` by ``padded_num_vertices``
times to user-specified divisor. So first we compute ``padded_num_vertices``,
again following the exact same algorithm that the hardware uses, then multiply
it by the GL-level divisor to get the hardware-level divisor. This case is
further divided into two more cases. If the hardware-level divisor is a
power of two, then we just need to shift. The shift amount is specified by
the shift field, so that the hardware-level divisor is just
:math:`2^\text{shift}`.
If it isn't a power of two, then we have to divide by an arbitrary integer.
For that, we use the well-known technique of multiplying by an approximation
of the inverse. The driver must compute the magic multiplier and shift
amount, and then the hardware does the multiplication and shift. The
hardware and driver also use the "round-down" optimization as described in
https://ridiculousfish.com/files/faster_unsigned_division_by_constants.pdf.
The hardware further assumes the multiplier is between :math:`2^{31}` and
:math:`2^{32}`, so the high bit is implicitly set to 1 even though it is set
to 0 by the driver -- presumably this simplifies the hardware multiplier a
little. The hardware first multiplies ``linear_id`` by the multiplier and
takes the high 32 bits, then applies the round-down correction if
:math:`\text{extra_flags} = 1`, then finally shifts right by the shift field.
There are some differences between ridiculousfish's algorithm and the Mali
hardware algorithm, which means that the reference code from ridiculousfish
doesn't always produce the right constants. Mali does not use the pre-shift
optimization, since that would make a hardware implementation slower (it
would have to always do the pre-shift, multiply, and post-shift operations).
It also forces the multiplier to be at least :math:`2^{31}`, which means
that the exponent is entirely fixed, so there is no trial-and-error.
Altogether, given the divisor d, the algorithm the driver must follow is:
1. Set :math:`\text{shift} = \lfloor \log_2(d) \rfloor`.
2. Compute :math:`m = \lceil 2^{shift + 32} / d \rceil` and :math:`e = 2^{shift + 32} % d`.
3. If :math:`e \leq 2^{shift}`, then we need to use the round-down algorithm.
Set :math:`\text{magic_divisor} = m - 1` and :math:`\text{extra_flags} = 1`.
4. Otherwise, set :math:`\text{magic_divisor} = m` and
:math:`\text{extra_flags} = 0`.
+17
View File
@@ -0,0 +1,17 @@
Compressed texture support
==========================
In the driver, Panfrost supports ASTC, ETC, and all BCn formats (e.g. RGTC,
S3TC, etc.) However, Panfrost depends on the hardware to support these formats
efficiently. All supported Mali architectures support these formats, but not
every system-on-chip with a Mali GPU support all these formats. Many lower-end
systems lack support for some BCn formats, which can cause problems when playing
desktop games with Panfrost. To check whether this issue applies to your
system-on-chip, Panfrost includes a ``panfrost_texfeatures`` tool to query
supported formats.
To use this tool, include the option ``-Dtools=panfrost`` when configuring Mesa.
Then inside your Mesa build directory, the tool is located at
``src/panfrost/tools/panfrost_texfeatures``. Copy it to your target device,
set as executable as necessary, and run on the target device. A table of
supported formats will be printed to standard output.
+37
View File
@@ -0,0 +1,37 @@
U-interleaved tiling
====================
Panfrost supports u-interleaved tiling. U-interleaved tiling is
indicated by the ``DRM_FORMAT_MOD_ARM_16X16_BLOCK_U_INTERLEAVED`` modifier.
The tiling reorders whole pixels (blocks). It does not compress or modify the
pixels themselves, so it can be used for any image format. Internally, images
are divided into tiles. Tiles occur in source order, but pixels (blocks) within
each tile are reordered according to a space-filling curve.
For regular formats, 16x16 tiles are used. This harmonizes with the default tile
size for binning and CRCs (transaction elimination). It also means a single line
(16 pixels) at 4 bytes per pixel equals a single 64-byte cache line.
For formats that are already block compressed (S3TC, RGTC, etc), 4x4 tiles are
used, where entire blocks are reorder. Most of these formats compress 4x4
blocks, so this gives an effective 16x16 tiling. This justifies the tile size
intuitively, though it's not a rule: ASTC may uses larger blocks.
Within a tile, the X and Y bits are interleaved (like Morton order), but with a
twist: adjacent bit pairs are XORed. The reason to add XORs is not obvious.
Visually, addresses take the form::
| y3 | (x3 ^ y3) | y2 | (y2 ^ x2) | y1 | (y1 ^ x1) | y0 | (y0 ^ x0) |
Reference routines to encode/decode u-interleaved images are available in
``src/panfrost/shared/test/test-tiling.cpp``, which documents the space-filling
curve. This reference implementation is used to unit test the optimized
implementation used in production. The optimized implementation is available in
``src/panfrost/shared/pan_tiling.c``.
Although these routines are part of Panfrost, they are also used by Lima, as Arm
introduced the format with Utgard. It is the only tiling supported on Utgard. On
Mali-T760 and newer, Arm Framebuffer Compression (AFBC) is more efficient and
should be used instead where possible. However, not all formats are
compressible, so u-interleaved tiling remains an important fallback on Panfrost.
+108
View File
@@ -0,0 +1,108 @@
Varyings
========
This is the naming convention followed by the hardware (slightly different
from Vulkan naming):
- "Attribute": per-vertex data
- "Varying": per-fragment data
Thus vertex shaders always interact with attributes and fragment/pixel shaders
only interact with varyings (commonly shortened to "VAR").
Hardware descriptors
--------------------
Before Valhall there were two types of hardware descriptors:
- ``AttributeDescriptor``: Descriptor for a single attribute in a larger buffer,
contains offset, stride and the in-memory format.
- ``BufferDescriptor``: Buffer, when used for attributes and varyings it
contains: pointer, size and stride
Midgard (v5)
------------
Attributes are loaded/stored using ``LD_ATTR``/``ST_ATTR``, those query the
attribute descriptors and buffer descriptors to find the real global memory
offset before loading/storing.
All varyings are loaded using ``LD_VAR``, including flat attributes (using a
modifier), and special attributes (position, psiz, ecc...). Special attributes
require a special descriptor, that is not backed by any buffer, it generates
data on-the-fly.
There is a concept of in-memory format and register format, the first is written
inside of the ``AttributeDescriptor`` while the register format is a modifier of
the instructions, when those two disagree the hardware will try to convert them,
this is only supported between int-int or float-float operations.
Bifrost (v6)
------------
Changes:
- ``ST_ATTR`` is replaced by ``LEA_ATTR`` and ``ST_CVT`` pairs.
- ``LEA_ATTR`` queries memory address and format from the appropriate
descriptor.
- ``ST_CVT`` performs the actual conversion and memory store.
- A new ``LD_VAR_SPECIAL`` is introduced, now special attributes do not need
special descriptors.
- ``LD_VAR`` only handles interpolated varyings, flat varyings are now handled
by ``LD_VAR_FLAT``.
Valhall (v9)
------------
Valhall introduced layout-specific instructions, ``LEA_BUF`` and ``LD_VAR_BUF``,
those do not use ``AttributeDescriptor`` but read ``BufferDescriptor`` directly.
These new instructions bake the offset and in-memory format of the attribute
inside of the shader code itself, saving a lookup. If all shaders in a program
use those instructions, we can remove all ``AttributeDescriptor``. If the
shaders aren't compiled together, one of the shaders cannot know the data layout,
and it needs to fall back to the old ``AttributeDescriptor``-based path. We
also have to fall back to ``AttributeDescriptor`` completely if the attribute
offset overflows the immediate field.
``LD_VAR_BUF`` only supports ``.f16`` and ``.f32`` as register modifiers, we can
also load (flat) ints with those. We need to specify no conversion
(``.f16.src_flat16`` or ``.f32.src_flat32``), but with those we can load both
16-bit and 32-bit integers.
In this architecture we assume the VS always uses IDVS, emitting attribute data
with ``LEA_BUF`` (instead of ``LEA_ATTR``), and never emit varying
``AttributeDescriptor`` for VS.
Challenges
----------
Theoretically, the output types from the VS and input types from the FS should
always agree. In practice we cannot trust the varyings types given to us,
here are some challenging examples:
* Shaders are allowed to use the same types with different precision modifiers.
* ``nir_opt_varying`` erases highp flat types with "float32" and packs them
together.
* Some games like to play with ``intBitsToFloat`` / ``floatBitsToInt``, so it's
possible to have VS write int and FS read float (in that case, we treat the
underlying bits as float, allowing for NaN normalization)
In practice you have to follow these rules:
* Only FS knows a varying smoothness, VS sees everything as flat.
* When linked together, you cannot trust highp flat types, but you can trust
precision
* While loading/storing data, mistaking a float for an int is ok, mistaking an
int for a float seems to be lossy even without bit-size conversions. Be
conservative on what you say is a float.
* You cannot lower flat varyings after they passed through ``nir_opt_varying``
Current implementation rules:
* VS decides the memory layout
* Descriptor formats can mismatch between stages (eg. VS int - FS float)
* Descriptor formats always match what the instructions are using
To regain the type information:
* in VS, every highp varying is written as U32
* in FS, highp flat varyings are read as U32, highp smooth are read as F32
* for mediump, we trust the original type