Textures and tensors¶
Some instructions need more than a direct rewrite. Texture reads need a program's image descriptions turned into Vulkan images. Tensor core instructions, which multiply small matrices in one go, need Ampere's own way of adding numbers for bit-exact results. Exact mode uses that method; the default uses the faster cooperative path.
Textures¶
How does a shader find its texture?¶
A texture is an image a shader reads with filtering, and a sampler holds the reading settings, such as edge wrapping. A program describes both in tables in emulated RAM, called pools. The TIC pool holds texture headers (format, size, layout, address) and the TSC pool holds sampler entries.
The shader picks one entry from each pool with a handle, one word stored in a constant buffer (cbuf). The translator records where each handle comes from, as a cbuf bank and byte offset. Each distinct source gets its own Vulkan slot, a binding, numbered upwards from 36. Before the run, the PC reads the handle, decodes both pool entries, and plugs in a Vulkan image with its sampler. The image is uploaded again whenever its emulated pages change, so a texture that was drawn into shows its latest draw. In draws, textures are read by pixel shaders today.
| Handle bits | Picks |
|---|---|
| Low 20 | The TIC entry, the texture |
| Top 12 | The TSC entry, the sampler, unless the game links each sampler to its texture's index |
flowchart LR
CB["Handle word<br/>In a constant buffer"] --> TIC["Texture header<br/>Format, size, layout, mip levels"]
CB --> TSC["Sampler entry<br/>Edge wrap, filtering, detail level"]
TIC --> DET{"How are the pixels laid out?"}
DET -->|"Plain rows"| PL["Copy rows as they are<br/>One level"]
DET -->|"NVIDIA tiles"| BL["Untangle the tiles<br/>Per mip level"]
PL --> IMG["Vulkan image"]
BL --> IMG
TSC --> SMP["Vulkan sampler"]
IMG --> BIND["Texture slot<br/>Binding 12 and up"]
SMP --> BIND
How one handle word becomes a Vulkan image and sampler in a shader slot.
Which textures are supported?¶
| Decoded | Supported |
|---|---|
| Layouts | Plain rows (pitch-linear) with one level; NVIDIA tiles (block-linear) with a chain of mip levels |
| Formats | A8B8G8R8 UNORM and sRGB, R8, G8R8, R16G16B16A16 float, R32 UINT, compressed BC1, BC3, BC7 |
| Type and channel order | 2D, channels in their normal order |
| Sampler | Repeat or clamp to edge, nearest or linear filtering, mip filter and detail clamp |
| Rejected by name | Other formats, channel orders and types, depth compare, anisotropy, reduction filters, unnormalized sampler coordinates |
A mip level is a smaller copy of the texture, used when it is far away. sRGB is a colour encoding that matches how screens show brightness.
What is a block-linear texture?¶
Block-linear is NVIDIA's tiled layout. It keeps pixels that are close on screen close in memory, which makes reads faster. The tiles are GOBs, groups of bytes, and GOBs stack into taller blocks. The decoder untangles the tiles back into rows.
| Block-linear fact | Value |
|---|---|
| GOB size | 64 bytes wide, 8 rows high |
| Inside a GOB | 16-byte pieces, interleaved |
| Blocks | GOBs stacked along Y |
| Block height per mip level | Shrinks as the level shrinks, the way Ryujinx's size calculator does |
Which instructions read textures?¶
| Instructions | Vulkan form |
|---|---|
| TEX, TLD, TLD4, TXD, TXQ, TMML: sampled reads and queries | Combined image samplers |
| SULD, SUST, SURED, SUATOM: pixel loads, stores and atomics | Storage images |
TMML asks which mip level a read would use. In compute it gets its slopes from Vulkan's compute-shader derivative groups.
Why record where the handle comes from, not its value: the source is known when translating, but the value is only known at run time. A handle whose source cannot be traced fails by name.
Tensors¶
How are matrix instructions run?¶
Tensor core instructions (HMMA, IMMA, DMMA, BMMA, LDSM and MOVM) compute D = A × B + C on small matrices. The matrices are spread across a warp, the team of threads that runs in lockstep. Each thread holds a few elements, its matrix fragment. The translator runs them per thread and shares each input register through one exchange array.
| Layout fact | Value |
|---|---|
| Which thread holds what | Thread 4g + t holds row g of a fragment |
| Sharing | Each source register goes through one warp exchange |
How does the chip add the products?¶
Standard float maths adds one number at a time and rounds after each step. The real chip does not sum HMMA products that way. Exact mode reproduces its method bit for bit:
| Step | Rule |
|---|---|
| Grouping | Products in groups of 8 along k (4 for TF32); C joins the group sum |
| Alignment | Every term is lined up to the largest exponent; a product's exponent is ea + eb |
| Truncation | Bits below 2^(emax-24) are dropped, not rounded |
| Sum | Exact integer sum, turned back into f32 by truncation |
| .F16 result | Each group rounds to f16 |
| Inf and NaN | Standard float rules, applied to only the special terms |
Why is the sum written as loops?¶
Written straight out, the exact method makes a huge shader that drivers take long to compile. Putting it in one SPIR-V function did not help, because both drivers pasted it back into every caller. So the inputs are loaded once into a per-thread array. The sum runs in loops marked DontUnroll, a hint that tells the driver to keep each loop as a loop. KOKOROMI_EXACT=1 selects this path. See ADR-0005.
Per HMMA.16816.F32, cold pipeline compile:
| Form | SPIR-V words | Intel | lavapipe |
|---|---|---|---|
| Standard float order, unrolled | 3.1k | 18 ms | 0.8 s |
| Exact method, unrolled | 15.8k | 0.6 to 1 s | 2.5 s |
| Exact method, one function | 3.1k | 940 ms | 6.0 s |
| Exact method, four DontUnroll loops | 4.9k | 57 ms | 0.2 s |
Verified by: the tensor silicon scoreboard, recordings from a real card, passing on Intel and lavapipe; texture and image recordings from CUDA texture objects; and the block-linear untangling checked against the Python asset converter.
Sources: core/src/gpu/texture.rs, core/src/gpu/sm86/{texture,tensor}.rs, core/src/tests/gpu/{texture,tensor}.rs.