docs(twitter): add 1859395250537595016 corpus (NOTimothyLottes hard-core shader dev mega-thread)

33-post thread, 13 PNGs, the canonical 'shader dev' walkthrough. Reads
FXAA 3.11 / FSR1 / Unity STP, names the permutations pattern (32-bit /
packed 16-bit / implicit mediump / MIN-MAX sampling). DXIL no bitfield
ops vs SPIR-V (HLSL pre-processing on non-Xbox = fail). 16-bit
perf: don't permute from 32-bit float constants (instant PC perf
death), alias FP16 as UINT32 binary blobs. Designing shaders like
GPU assembly, AMD RDNA2 as the design target. Defines map logic to
associated instruction (fma, bitfieldExtract), all-ints-unsigned
convention with SI1_I1()-style bitcast macros, 3-letter type macros.
Compiler pattern-match bugs: tried always-UINT4 bitcast, didn't
work; native types except waveops. Argument passing SSA state
explosion: inout uint4[16] (all VGPRs) didn't end well - shader langs
support globals, use them. Next level: mostly globals with type-
bitcast aliasing; x86-64 union-of-structs-on-globals as the GPU
calling ABI. Foundation for the SPIR-V-from-data + cart-file work
(Aug 2025).
This commit is contained in:
ed
2026-07-27 01:52:23 -04:00
parent a9335928c7
commit aef260d907
15 changed files with 733 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

+173
View File
@@ -0,0 +1,173 @@
---
title: "[32] oh damn [32], should probably stop now, until next time."
author: "NOTimothyLottes"
handle: "@NOTimothyLottes"
post_url: "https://x.com/NOTimothyLottes/status/1859395250537595016"
post_id: "1859395250537595016"
timestamp: "2024-11-21 00:35:48"
post_count: 33
reply_count: 0
repost_count: 0
like_count: 8
view_count: 676
---
# @NOTimothyLottes — [32] oh damn [32], should probably stop now, until next time.
## Post 1 (2024-11-20 22:51:23)
[0] Been wanting to do this for a long time, a thread on hard-core shader development. Something that digs into what I've learned and applied over the years. Along with the evolution of the strange syntax I use when writing shaders ...
## Post 2 (2024-11-20 22:55:26) — reply to Post 1
[1] Lets start at one of the early ones, FXAA 3.11. Someone is hosting that here: https://gist.github.com/kosua20/0c506b81b3812ac900048059d2383126 ... most lines, it's an easy map to NV's arch which has predicated instructions (red is the predicate). Many lines are 1:1 mappings with physical HW instructions.
![Media 1](./media/1859369992355021246_1.png)
## Post 3 (2024-11-20 23:03:28) — reply to Post 2
[2] Looking at FSR1: https://github.com/GPUOpen-Effects/FidelityFX-FSR/blob/master/ffx-fsr/ffx_fsr1.h - that had 3 permutations {32-bit, 16-bit unpacked, 16-bit packed}, where the packed one maps to how AMD's packed instructions work (everything explicitly packed)
![Media 1](./media/1859372015670432237_1.png)
## Post 4 (2024-11-20 23:08:26) — reply to Post 3
[3] Onto STP: https://github.com/Unity-Technologies/Graphics/blob/27341ce4f5c8853d7f15e9420bff499f1087aceb/Packages/com.unity.render-pipelines.core/Runtime/STP/Stp.hlsl - A different set of permutations {32-bit, packed 16-bit} then {implicit mediump vs explicit} in the 32-bit (generic) permutation via types like StpMF4 which aim to allow a compiler on mobile to get some 16-bit register savings
![Media 1](./media/1859373265380069827_1.png)
## Post 5 (2024-11-20 23:11:02) — reply to Post 4
[4] Notice for 16-bit path, everything again is explicitly packed (unlike the 32-bit path). And GLSL to SPIR-V would get explicit packing to the IHV driver since SPIR-V has explicit vector types (unlike DX, which was an insane problem). Example, the reprojection filtering below.
![Media 1](./media/1859373919146213617_1.png)
## Post 6 (2024-11-20 23:14:02) — reply to Post 5
[5] Then STP had defines to work out differences in sampling capability (like supporting MIN/MAX sampling with UINT or not, or supporting immediate offsets or not). Example in the code doing {z,motion} UINT32 packed nearest dilation:
![Media 1](./media/1859374673953239528_1.png)
## Post 7 (2024-11-20 23:22:35) — reply to Post 6
[6] The largest portability nightmare was mixing explicit FP16 with wave ops! So many compiler bugs across vendors = so many permutations in the code. The 32-bit path had to have a conversion from 'StpMF4' (possible implicit medium precision), to 'StpF4' explicit 32-bit.
![Media 1](./media/1859376826709311577_1.png)
## Post 8 (2024-11-20 23:26:00) — reply to Post 7
[7] Some of perf death problems had been that DXIL didn't have bitfield ops (unlike SPIR-V), HLSL depends on IHV compilers somehow finding and pattern matching stuff like the crap below (which is MANY OPS to 1 OPCODE). So pre-processing through HLSL on non-Xbox platforms = fail!
![Media 1](./media/1859377684104536314_1.png)
## Post 9 (2024-11-20 23:29:55) — reply to Post 8
[8] Go into the shader which is doing wave ops, and the true nightmare of platform workarounds becomes apparent https://github.com/Unity-Technologies/Graphics/blob/27341ce4f5c8853d7f15e9420bff499f1087aceb/Packages/com.unity.render-pipelines.core/Runtime/STP/StpSetup.compute - LSD/waveop permutations, 32-bit/16-bit in 16-bit path, etc
![Media 1](./media/1859378670105674093_1.png)
## Post 10 (2024-11-20 23:36:48) — reply to Post 9
[9] So yeah, even in modern times you can hit paths in compilers and toolchains that well never got tested because no one had tried it and tried shipping with it. And I think this might hint that relatively few actually do this level of crazy
## Post 11 (2024-11-20 23:40:23) — reply to Post 10
[10] Part of the formula for getting great gains with 16-bit is simply the register savings. It wouldn't be possible to do good 4x4 kernel lanczos inside a TAA without packed 16-bit support (it's 64 values on load without thinking about all the other mid-algorithm data) ...
## Post 12 (2024-11-20 23:42:12) — reply to Post 11
[11] Looking at STP again, there is a dering, which works with the {min,max} of the near 2x2. If the HW has MIN/MAX sampling, it can fetch that, which means a lot more than 64 values, and if not they need intermediate ALU min/max, which is extra state ...
![Media 1](./media/1859381761685221605_1.png)
## Post 13 (2024-11-20 23:45:41) — reply to Post 12
[12] Mobile hardware is so register starved (probably due to all that SRAM area going to tiles) that it wasn't possible to do good reprojection filtering PERIOD even with FP16. So STP had a crippled compromise that did bilinear in one direction, and lanczos in the other axis
![Media 1](./media/1859382640534516057_1.png)
## Post 14 (2024-11-20 23:47:49) — reply to Post 13
[13] Huge wake up call to mobile HW vendors, if you don't match VGPR capacity of PCs, you can never take on where PC workloads will get to. Now given both NV and AMD have packed 16-bit, it is a great time to push pass merging and avoid DRAM traffic by doing more in one shader
## Post 15 (2024-11-20 23:50:55) — reply to Post 14
[14] Another part of the 16-bit perf puzzle is to not use 16-bit FP16 in constants explicitly, but only package them up as aliased as UINT32 binary blobs. And for portable systems that still need 32-bit fallbacks, always build both a packed 16-bit and 32-bit set of constants.
## Post 16 (2024-11-20 23:52:28) — reply to Post 15
[15] So many people tried to 16-bit permute things working from 32-bit float constants, and on PC that is instant perf death. Sure some mobile hardware can do the conversions for lower cost, and some mobile HW could pre-amble the conversions if not, but that isn't perf portable
## Post 17 (2024-11-20 23:55:35) — reply to Post 16
[16] Switching gears. So STP didn't get the most hardcore way I like to write shaders, and I've had a bunch of different permutations of ideas tried over the years ... specifically how to make the code more like GPU assembly!
## Post 18 (2024-11-20 23:57:03) — reply to Post 17
[17] Looking like assembly as in making it exactly clear what the lines map to in HW OPCODEs, and what the variable map to in REGISTERS. So you are designing to what the hardware could do, even if the compiler cannot keep up
## Post 19 (2024-11-20 23:59:37) — reply to Post 18
[18] Now it's true that NV and AMD HW are quite different in some respects (NV is predicated, and has generic 3 op logic ops, etc), so ultimately one has to pick their shader language abstraction based on what they want to target the most https://docs.nvidia.com/cuda/cuda-binary-utilities/index.html#instruction-set-reference (NV ISA docs)
## Post 20 (2024-11-21 00:01:08) — reply to Post 19
[19] Since AMD's docs are more complete, and I like to make sure a console port is possible and would be as fast as possible, I prefer to think through all my shaders as if they are getting direct mapped to AMD's RDNA2 these days: https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna2-shader-instruction-set-architecture.pdf
## Post 21 (2024-11-21 00:06:04) — reply to Post 20
[20] So I usually do a define that maps logic directly to what the associated instruction would be. Some of these actually map to shading language intrinsics already like fma() or bitfieldExtract(). Then write all my shader code using these defines. Example below for the defines
![Media 1](./media/1859387770617110636_1.png)
## Post 22 (2024-11-21 00:10:21) — reply to Post 21
[21] Like to use ASM like convention for {signed, unsigned}, that all integers are unsigned, and only get converted to signed when required inside the define for a specific opcode (like signed or unsigned bitfield extract). Bit casting (aliasing) is done via SI1_I1() style macros
![Media 1](./media/1859388846481322425_1.png)
## Post 23 (2024-11-21 00:12:49) — reply to Post 22
[22] And I reduce all types to very simple 3 letter macros of which are described below. I use the same naming convention in CPU land as well. This helps keep code compact and easy to read IMO (highly subjective yes)
![Media 1](./media/1859389468672680384_1.png)
## Post 24 (2024-11-21 00:16:45) — reply to Post 23
[23] One thing I struggle with to this day is compiler bitcast aliasing perf bugs. Meaning once I tried to always only use UINT4 types everywhere, and then wrap all OPCODE defines in bitcasting defines, that didn't go so well (looking at disassembly on AMD PC).
## Post 25 (2024-11-21 00:18:13) — reply to Post 24
[24] Compilers depend a lot on pattern matching to NOP stuff, and well there are bugs, lots of bugs. So for the sake of portability, with the exception of {unsigned/signed} bitcasting, I use the native types when possible (no packed-16-bit/32-bit aliasing) exception of waveops
## Post 26 (2024-11-21 00:21:55) — reply to Post 25
[25] Another aspect of compiler problems is argument passing to function calls causing SSA state explosions, because it's "copy semantics" as defined in the langs. Once I did inout uint4[16] (all VGPRs) as my only argument to all functions. Didn't end well!
## Post 27 (2024-11-21 00:23:26) — reply to Post 26
[26] Someone on twitter (or was it bsky or mast?) had been doing a one shader game. And addressing these kinds of problems, and reminded us, YES the shader languages support global variables. No need to pass them into function! (bingo)
## Post 28 (2024-11-21 00:24:52) — reply to Post 27
[27] We have been so seasoned to dislike globals in the past few decades due to well probably early mutli-window programming styles, that it is easy to forget you can just do globals, and they are fast at tool/compile time.
## Post 29 (2024-11-21 00:26:42) — reply to Post 28
[28] I think the next level up of this design is to go to mostly global variables, but since the shader languages don't support global unions, then have different sets of globals when one needs to do type bitcast aliasing (which is very common with packed 16-bit and 64-bit logic)
## Post 30 (2024-11-21 00:28:59) — reply to Post 29
[29] I still think the 'fix' for the shader compiler SSA state explosion mapping back to registers problem is to pass in explicit aliasing hints to the vendor compilers, so they have a better base guess at register allocation. But I doubt vendors would get on board with that
## Post 31 (2024-11-21 00:31:24) — reply to Post 30
[30] My personal x86-64 languages typically worked with unions of structs on globals. Where each struct in the union was a different aliased meaning to the HW registers, and functions had been associated with a given struct in the union. So no stacks or argument passing.
## Post 32 (2024-11-21 00:33:27) — reply to Post 31
[31] That kind of setup for the GPU is ideal IMO (at least for the type of stuff I do). You can link. Because the union struct for the registers is the calling ABI, and never moves (no stack). So code sharing works amazingly well. And avoiding register state explosion is built in
## Post 33 (2024-11-21 00:35:48) — reply to Post 32
[32] oh damn [32], should probably stop now, until next time.
@@ -0,0 +1,560 @@
{
"root_post_id": "1859395250537595016",
"posts": [
{
"post_id": "1859368974351274470",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[0] Been wanting to do this for a long time, a thread on hard-core shader development. Something that digs into what I've learned and applied over the years. Along with the evolution of the strange syntax I use when writing shaders ...",
"timestamp": "2024-11-20 22:51:23",
"media_urls": [],
"reply_to_id": null,
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 22,
"like_count": 195,
"view_count": 19712
}
},
{
"post_id": "1859369992355021246",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[1] Lets start at one of the early ones, FXAA 3.11. Someone is hosting that here: https://gist.github.com/kosua20/0c506b81b3812ac900048059d2383126 ... most lines, it's an easy map to NV's arch which has predicated instructions (red is the predicate). Many lines are 1:1 mappings with physical HW instructions.",
"timestamp": "2024-11-20 22:55:26",
"media_urls": [
"https://pbs.twimg.com/media/Gc3OqZkWsAAeP9s?format=png&name=orig"
],
"reply_to_id": "1859368974351274470",
"quote_of_id": null,
"metrics": {
"reply_count": 2,
"repost_count": 1,
"like_count": 10,
"view_count": 2078
}
},
{
"post_id": "1859372015670432237",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[2] Looking at FSR1: https://github.com/GPUOpen-Effects/FidelityFX-FSR/blob/master/ffx-fsr/ffx_fsr1.h - that had 3 permutations {32-bit, 16-bit unpacked, 16-bit packed}, where the packed one maps to how AMD's packed instructions work (everything explicitly packed)",
"timestamp": "2024-11-20 23:03:28",
"media_urls": [
"https://pbs.twimg.com/media/Gc3QjEiWYAADo14?format=png&name=orig"
],
"reply_to_id": "1859369992355021246",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 1,
"like_count": 10,
"view_count": 1624
}
},
{
"post_id": "1859373265380069827",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[3] Onto STP: https://github.com/Unity-Technologies/Graphics/blob/27341ce4f5c8853d7f15e9420bff499f1087aceb/Packages/com.unity.render-pipelines.core/Runtime/STP/Stp.hlsl - A different set of permutations {32-bit, packed 16-bit} then {implicit mediump vs explicit} in the 32-bit (generic) permutation via types like StpMF4 which aim to allow a compiler on mobile to get some 16-bit register savings",
"timestamp": "2024-11-20 23:08:26",
"media_urls": [
"https://pbs.twimg.com/media/Gc3SFXTXoAEt4tX?format=png&name=orig"
],
"reply_to_id": "1859372015670432237",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 4,
"view_count": 1529
}
},
{
"post_id": "1859373919146213617",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[4] Notice for 16-bit path, everything again is explicitly packed (unlike the 32-bit path). And GLSL to SPIR-V would get explicit packing to the IHV driver since SPIR-V has explicit vector types (unlike DX, which was an insane problem). Example, the reprojection filtering below.",
"timestamp": "2024-11-20 23:11:02",
"media_urls": [
"https://pbs.twimg.com/media/Gc3SmjPWMAEMX_d?format=png&name=orig"
],
"reply_to_id": "1859373265380069827",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 5,
"view_count": 1397
}
},
{
"post_id": "1859374673953239528",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[5] Then STP had defines to work out differences in sampling capability (like supporting MIN/MAX sampling with UINT or not, or supporting immediate offsets or not). Example in the code doing {z,motion} UINT32 packed nearest dilation:",
"timestamp": "2024-11-20 23:14:02",
"media_urls": [
"https://pbs.twimg.com/media/Gc3TW0oWIAArDjw?format=png&name=orig"
],
"reply_to_id": "1859373919146213617",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 3,
"view_count": 1266
}
},
{
"post_id": "1859376826709311577",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[6] The largest portability nightmare was mixing explicit FP16 with wave ops! So many compiler bugs across vendors = so many permutations in the code. The 32-bit path had to have a conversion from 'StpMF4' (possible implicit medium precision), to 'StpF4' explicit 32-bit.",
"timestamp": "2024-11-20 23:22:35",
"media_urls": [
"https://pbs.twimg.com/media/Gc3UTC_WcAAyc5J?format=png&name=orig"
],
"reply_to_id": "1859374673953239528",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 2,
"view_count": 1237
}
},
{
"post_id": "1859377684104536314",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[7] Some of perf death problems had been that DXIL didn't have bitfield ops (unlike SPIR-V), HLSL depends on IHV compilers somehow finding and pattern matching stuff like the crap below (which is MANY OPS to 1 OPCODE). So pre-processing through HLSL on non-Xbox platforms = fail!",
"timestamp": "2024-11-20 23:26:00",
"media_urls": [
"https://pbs.twimg.com/media/Gc3V5wGW8AADAju?format=png&name=orig"
],
"reply_to_id": "1859376826709311577",
"quote_of_id": null,
"metrics": {
"reply_count": 2,
"repost_count": 0,
"like_count": 5,
"view_count": 1310
}
},
{
"post_id": "1859378670105674093",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[8] Go into the shader which is doing wave ops, and the true nightmare of platform workarounds becomes apparent https://github.com/Unity-Technologies/Graphics/blob/27341ce4f5c8853d7f15e9420bff499f1087aceb/Packages/com.unity.render-pipelines.core/Runtime/STP/StpSetup.compute - LSD/waveop permutations, 32-bit/16-bit in 16-bit path, etc",
"timestamp": "2024-11-20 23:29:55",
"media_urls": [
"https://pbs.twimg.com/media/Gc3W143XEAEq6LS?format=png&name=orig"
],
"reply_to_id": "1859377684104536314",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 5,
"view_count": 1289
}
},
{
"post_id": "1859380404781814109",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[9] So yeah, even in modern times you can hit paths in compilers and toolchains that well never got tested because no one had tried it and tried shipping with it. And I think this might hint that relatively few actually do this level of crazy",
"timestamp": "2024-11-20 23:36:48",
"media_urls": [],
"reply_to_id": "1859378670105674093",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 4,
"view_count": 1827
}
},
{
"post_id": "1859381304112054632",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[10] Part of the formula for getting great gains with 16-bit is simply the register savings. It wouldn't be possible to do good 4x4 kernel lanczos inside a TAA without packed 16-bit support (it's 64 values on load without thinking about all the other mid-algorithm data) ...",
"timestamp": "2024-11-20 23:40:23",
"media_urls": [],
"reply_to_id": "1859380404781814109",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 5,
"view_count": 901
}
},
{
"post_id": "1859381761685221605",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[11] Looking at STP again, there is a dering, which works with the {min,max} of the near 2x2. If the HW has MIN/MAX sampling, it can fetch that, which means a lot more than 64 values, and if not they need intermediate ALU min/max, which is extra state ...",
"timestamp": "2024-11-20 23:42:12",
"media_urls": [
"https://pbs.twimg.com/media/Gc3Ze31XAAA94Y1?format=png&name=orig"
],
"reply_to_id": "1859381304112054632",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 3,
"view_count": 908
}
},
{
"post_id": "1859382640534516057",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[12] Mobile hardware is so register starved (probably due to all that SRAM area going to tiles) that it wasn't possible to do good reprojection filtering PERIOD even with FP16. So STP had a crippled compromise that did bilinear in one direction, and lanczos in the other axis",
"timestamp": "2024-11-20 23:45:41",
"media_urls": [
"https://pbs.twimg.com/media/Gc3afacXQAAFX-c?format=png&name=orig"
],
"reply_to_id": "1859381761685221605",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 2,
"view_count": 883
}
},
{
"post_id": "1859383175849349534",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[13] Huge wake up call to mobile HW vendors, if you don't match VGPR capacity of PCs, you can never take on where PC workloads will get to. Now given both NV and AMD have packed 16-bit, it is a great time to push pass merging and avoid DRAM traffic by doing more in one shader",
"timestamp": "2024-11-20 23:47:49",
"media_urls": [],
"reply_to_id": "1859382640534516057",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 3,
"view_count": 728
}
},
{
"post_id": "1859383954551243222",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[14] Another part of the 16-bit perf puzzle is to not use 16-bit FP16 in constants explicitly, but only package them up as aliased as UINT32 binary blobs. And for portable systems that still need 32-bit fallbacks, always build both a packed 16-bit and 32-bit set of constants.",
"timestamp": "2024-11-20 23:50:55",
"media_urls": [],
"reply_to_id": "1859383175849349534",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 2,
"view_count": 699
}
},
{
"post_id": "1859384348249608376",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[15] So many people tried to 16-bit permute things working from 32-bit float constants, and on PC that is instant perf death. Sure some mobile hardware can do the conversions for lower cost, and some mobile HW could pre-amble the conversions if not, but that isn't perf portable",
"timestamp": "2024-11-20 23:52:28",
"media_urls": [],
"reply_to_id": "1859383954551243222",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 2,
"view_count": 678
}
},
{
"post_id": "1859385131942961199",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[16] Switching gears. So STP didn't get the most hardcore way I like to write shaders, and I've had a bunch of different permutations of ideas tried over the years ... specifically how to make the code more like GPU assembly!",
"timestamp": "2024-11-20 23:55:35",
"media_urls": [],
"reply_to_id": "1859384348249608376",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 2,
"view_count": 678
}
},
{
"post_id": "1859385497916932274",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[17] Looking like assembly as in making it exactly clear what the lines map to in HW OPCODEs, and what the variable map to in REGISTERS. So you are designing to what the hardware could do, even if the compiler cannot keep up",
"timestamp": "2024-11-20 23:57:03",
"media_urls": [],
"reply_to_id": "1859385131942961199",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 2,
"view_count": 645
}
},
{
"post_id": "1859386143663411375",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[18] Now it's true that NV and AMD HW are quite different in some respects (NV is predicated, and has generic 3 op logic ops, etc), so ultimately one has to pick their shader language abstraction based on what they want to target the most https://docs.nvidia.com/cuda/cuda-binary-utilities/index.html#instruction-set-reference (NV ISA docs)",
"timestamp": "2024-11-20 23:59:37",
"media_urls": [],
"reply_to_id": "1859385497916932274",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 2,
"view_count": 658
}
},
{
"post_id": "1859386525982527578",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[19] Since AMD's docs are more complete, and I like to make sure a console port is possible and would be as fast as possible, I prefer to think through all my shaders as if they are getting direct mapped to AMD's RDNA2 these days: https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna2-shader-instruction-set-architecture.pdf",
"timestamp": "2024-11-21 00:01:08",
"media_urls": [],
"reply_to_id": "1859386143663411375",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 3,
"view_count": 674
}
},
{
"post_id": "1859387770617110636",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[20] So I usually do a define that maps logic directly to what the associated instruction would be. Some of these actually map to shading language intrinsics already like fma() or bitfieldExtract(). Then write all my shader code using these defines. Example below for the defines",
"timestamp": "2024-11-21 00:06:04",
"media_urls": [
"https://pbs.twimg.com/media/Gc3fKzqXUAAGnBQ?format=png&name=orig"
],
"reply_to_id": "1859386525982527578",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 1,
"like_count": 7,
"view_count": 861
}
},
{
"post_id": "1859388846481322425",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[21] Like to use ASM like convention for {signed, unsigned}, that all integers are unsigned, and only get converted to signed when required inside the define for a specific opcode (like signed or unsigned bitfield extract). Bit casting (aliasing) is done via SI1_I1() style macros",
"timestamp": "2024-11-21 00:10:21",
"media_urls": [
"https://pbs.twimg.com/media/Gc3gGUTWQAAZwyX?format=png&name=orig"
],
"reply_to_id": "1859387770617110636",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 3,
"view_count": 637
}
},
{
"post_id": "1859389468672680384",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[22] And I reduce all types to very simple 3 letter macros of which are described below. I use the same naming convention in CPU land as well. This helps keep code compact and easy to read IMO (highly subjective yes)",
"timestamp": "2024-11-21 00:12:49",
"media_urls": [
"https://pbs.twimg.com/media/Gc3gna_XcAA7PXx?format=png&name=orig"
],
"reply_to_id": "1859388846481322425",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 4,
"view_count": 601
}
},
{
"post_id": "1859390458293547232",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[23] One thing I struggle with to this day is compiler bitcast aliasing perf bugs. Meaning once I tried to always only use UINT4 types everywhere, and then wrap all OPCODE defines in bitcasting defines, that didn't go so well (looking at disassembly on AMD PC).",
"timestamp": "2024-11-21 00:16:45",
"media_urls": [],
"reply_to_id": "1859389468672680384",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 3,
"view_count": 542
}
},
{
"post_id": "1859390826054299714",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[24] Compilers depend a lot on pattern matching to NOP stuff, and well there are bugs, lots of bugs. So for the sake of portability, with the exception of {unsigned/signed} bitcasting, I use the native types when possible (no packed-16-bit/32-bit aliasing) exception of waveops",
"timestamp": "2024-11-21 00:18:13",
"media_urls": [],
"reply_to_id": "1859390458293547232",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 2,
"view_count": 528
}
},
{
"post_id": "1859391758817247636",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[25] Another aspect of compiler problems is argument passing to function calls causing SSA state explosions, because it's \"copy semantics\" as defined in the langs. Once I did inout uint4[16] (all VGPRs) as my only argument to all functions. Didn't end well!",
"timestamp": "2024-11-21 00:21:55",
"media_urls": [],
"reply_to_id": "1859390826054299714",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 3,
"view_count": 518
}
},
{
"post_id": "1859392139790053772",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[26] Someone on twitter (or was it bsky or mast?) had been doing a one shader game. And addressing these kinds of problems, and reminded us, YES the shader languages support global variables. No need to pass them into function! (bingo)",
"timestamp": "2024-11-21 00:23:26",
"media_urls": [],
"reply_to_id": "1859391758817247636",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 4,
"view_count": 522
}
},
{
"post_id": "1859392498797261032",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[27] We have been so seasoned to dislike globals in the past few decades due to well probably early mutli-window programming styles, that it is easy to forget you can just do globals, and they are fast at tool/compile time.",
"timestamp": "2024-11-21 00:24:52",
"media_urls": [],
"reply_to_id": "1859392139790053772",
"quote_of_id": null,
"metrics": {
"reply_count": 2,
"repost_count": 1,
"like_count": 5,
"view_count": 803
}
},
{
"post_id": "1859392961294836222",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[28] I think the next level up of this design is to go to mostly global variables, but since the shader languages don't support global unions, then have different sets of globals when one needs to do type bitcast aliasing (which is very common with packed 16-bit and 64-bit logic)",
"timestamp": "2024-11-21 00:26:42",
"media_urls": [],
"reply_to_id": "1859392498797261032",
"quote_of_id": null,
"metrics": {
"reply_count": 2,
"repost_count": 0,
"like_count": 1,
"view_count": 594
}
},
{
"post_id": "1859393537214763223",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[29] I still think the 'fix' for the shader compiler SSA state explosion mapping back to registers problem is to pass in explicit aliasing hints to the vendor compilers, so they have a better base guess at register allocation. But I doubt vendors would get on board with that",
"timestamp": "2024-11-21 00:28:59",
"media_urls": [],
"reply_to_id": "1859392961294836222",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 1,
"view_count": 615
}
},
{
"post_id": "1859394143522521102",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[30] My personal x86-64 languages typically worked with unions of structs on globals. Where each struct in the union was a different aliased meaning to the HW registers, and functions had been associated with a given struct in the union. So no stacks or argument passing.",
"timestamp": "2024-11-21 00:31:24",
"media_urls": [],
"reply_to_id": "1859393537214763223",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 2,
"view_count": 599
}
},
{
"post_id": "1859394661518848470",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[31] That kind of setup for the GPU is ideal IMO (at least for the type of stuff I do). You can link. Because the union struct for the registers is the calling ABI, and never moves (no stack). So code sharing works amazingly well. And avoiding register state explosion is built in",
"timestamp": "2024-11-21 00:33:27",
"media_urls": [],
"reply_to_id": "1859394143522521102",
"quote_of_id": null,
"metrics": {
"reply_count": 1,
"repost_count": 0,
"like_count": 2,
"view_count": 767
}
},
{
"post_id": "1859395250537595016",
"author": "NOTimothyLottes",
"handle": "NOTimothyLottes",
"text": "[32] oh damn [32], should probably stop now, until next time.",
"timestamp": "2024-11-21 00:35:48",
"media_urls": [],
"reply_to_id": "1859394661518848470",
"quote_of_id": null,
"metrics": {
"reply_count": 0,
"repost_count": 0,
"like_count": 8,
"view_count": 676
}
}
],
"source_url": "https://x.com/NOTimothyLottes/status/1859395250537595016"
}