yum-mirror/slang
Making it easier to work with shaders
git clone https://git.yummers.dev/yum-mirror/slang
78d34f3b3
master
1// printing.slang 2 3// This file provides the GPU code for a simple library that 4// allows GPU shaders to print values to `stdout`. 5// 6// The implementation relies on a single buffer that must 7// be bound to any shader that uses GPU printing. 8// 9RWStructuredBuffer<uint> gPrintBuffer; 10// 11// Encoding 12// ======== 13// 14// The print buffer is organized in terms of 32-bit (`uint`) *words*. 15// 16// The first word in the print buffer is used as an atomic 17// counter, and must be initialized to zero before a shader starts. 18// By atomically incrementing this counter, GPU threads can allocate 19// space for printing commands in the buffer. All printing 20// commands are stored after the first word (so, starting at 21// an index of 1). 22// 23// A printing command starts with a single-word header, where 24// the high 32 bits specify the *op* for the command, and the 25// low 32 bits specify the number of *payload* words in in 26// the command. The payload is the words that immediately 27// follow the command header. 28// 29// Note that the header word for a command is *not* included 30// in the count of words in the low 16 bits. 31// 32// The opcode values need to be shared between CPU and GPU 33// code, so we use a bit of preprocessor trickery here to 34// generate an `enum` type with all the opcodes. 35// 36enum PrintingOp 37{ 38#define GPU_PRINTING_OP(NAME) NAME , 39#include "gpu-printing-ops.h" 40}; 41 42// It is critical that when printing something, we allocate 43// all the words it requires in the print buffer contiguously. 44// For example, if the user writes: 45// 46// println("Thread number ", threadID, " has value ", someValue); 47// 48// It would be very bad if the output from different threads 49// got interleaved, such that one cannot determine which value 50// goes with which thread. 51// 52// Allocating individual print *commands* atomically is not necessarily 53// enough: instead, we need to allocate the storage for all 54// the commands that comprise a `print()` call at once. 55// 56// The core allocation operation here is `_allocatePrintWords()` 57 58// Allocate space for one or more print commands. 59// 60uint _allocatePrintWords(uint wordCount) 61{ 62 // We allocate the required number of words with an atomic, and 63 // get back the old value of the counter, which tells us the 64 // offset at which the words for our printing operation should start. 65 // 66 uint wordOffset = 0; 67 InterlockedAdd(gPrintBuffer[0], wordCount, wordOffset); 68 69 // Because the first word of the buffer is reserved for the counter, 70 // and the counter value starts at zero, we need to add one to 71 // get to the actual offset for the data to be written. 72 // 73 return wordOffset + 1; 74} 75 76// Java-style `println` 77// ==================== 78// 79// We will start by building up a Java-style `println()` function 80// that accepts zero or more values to print, and prints them 81// atomically (without any other thread being able to interleave 82// in the printed output), followed by a newline. 83// 84// We will define a wrapper around `_allocatePrintWords()` 85// that captures the main idiom for `println()`. 86// 87uint _beginPrintln(uint wordCount) 88{ 89 // The `wordCount` passed in will represent the 90 // number of words required for the arguments 91 // to `println`, but won't include the terminating 92 // newline. 93 // 94 // Thus we will allocate one extra word to allow 95 // us to append a newline to the print command we 96 // generate. 97 // 98 uint wordOffset = _allocatePrintWords(wordCount + 1); 99 // 100 // We will then initialize the last word of the command 101 // that was allocated to a `NewLine` command. 102 // 103 gPrintBuffer[wordOffset + wordCount] = uint(PrintingOp.NewLine) << 16; 104 return wordOffset; 105} 106// 107// With the `_beginPrintLn()` function handling all the heavy-lifting, 108// we can define a zero-argument `println()` trivially. 109// 110void println() 111{ 112 _beginPrintln(0); 113} 114 115// We could continue to build a family of overloaded `println()` functions, like: 116// 117// void println(); 118// void println(int value); 119// void println(float value); 120// void println(uint value); 121// ... 122// 123// but it should be clear that this approach doesn't scale at all 124// to functions with multiple argumenst: 125// 126// void println(int a, int b); 127// void println(float a, int b); 128// void println(int a, float b); 129// ... 130// 131// Using the features of the Slang language, we can build a framework 132// for a more scalable solution. 133// 134// We start by defining an `interface` that captures the essence 135// of what a type of printable values needs to support. 136// 137 138interface IPrintable 139{ 140 // Every printable value needs to be able to compute the number 141 // of words required to write it into the print buffer. 142 // 143 uint getPrintWordCount(); 144 145 // A printable value must also support writing those words into 146 // a buffer, once the appropriate offset to write to is known. 147 // 148 void writePrintWords(RWStructuredBuffer<uint> buffer, uint offset); 149}; 150 151// With the `IPrintable` interface in place, we can now write 152// a generic one-argument `println()` that works with any 153// printable value. 154 155void println<T : IPrintable>(T value) 156{ 157 // In order to print a value we first compute the number of words 158 // it needs in the print buffer. 159 // 160 uint wordCount = value.getPrintWordCount(); 161 162 // Then we can use `_beginPrint()` to allocate those words and 163 // find the starting offset to write to. 164 // 165 uint wordOffset = _beginPrintln(wordCount); 166 167 // And finally we can ask the value to write itself into the 168 // buffer at the given offset. 169 // 170 value.writePrintWords(gPrintBuffer, wordOffset); 171} 172 173// Of course, in order to be able to print things with this `println()` 174// operation, we need to have some types that implement `IPrintable`. 175// 176// In particular, we'd like to be able to print built-in types like 177// `uint`, but we don't have access to the declaration of `uint` 178// to be able to change it! 179// 180// It just so happens that another Slang feature, `extension` 181// declarations, lets us extend a type with new methods *and* 182// allows us to add new interface implementations to it. 183// 184// We can therefore making the exisint Slang `uint` type be 185// printable. 186 187extension uint : IPrintable // <-- Note: we are adding a conformance to `IPrintable here` 188{ 189 // Printing a `uint` uses up two words in the buffer 190 // 191 uint getPrintWordCount() { return 2; } 192 193 // Writing a command to print a `uint` is straightforward, 194 // given knowledge of our encoding. 195 // 196 void writePrintWords(RWStructuredBuffer<uint> buffer, uint offset) 197 { 198 buffer[offset++] = (uint(PrintingOp.UInt32) << 16) | 1; 199 buffer[offset++] = this; 200 } 201} 202 203extension String : IPrintable 204{ 205 uint getPrintWordCount() { return 2; } 206 207 void writePrintWords(RWStructuredBuffer<uint> buffer, uint offset) 208 { 209 buffer[offset++] = (uint(PrintingOp.String) << 16) | 1; 210 buffer[offset++] = getStringHash(this); 211 } 212} 213 214// Where generics and interfaces start to pay off is when we want 215// to scale up to a two-argument `println()` function that can 216// work for any combination of printable types. 217 218// Print two values, `a` and `b`. 219// 220// This function ensures that the values of `a` and `b` 221// are written out atomically, without values printed 222// from other threads spliced in between. 223// 224void println<A : IPrintable, B : IPrintable>(A a, B b) 225{ 226 // To print two values atomically, we must first 227 // allocate the total number of words that are 228 // required to print the values. 229 // 230 uint wordCount = 0; 231 uint aCount = a.getPrintWordCount(); wordCount += aCount; 232 uint bCount = b.getPrintWordCount(); wordCount += bCount; 233 234 // Then we can allocate those words atomically 235 // with a single `_beginPrint()`. 236 // 237 uint wordOffset = _beginPrintln(wordCount); 238 239 // Finally, we can write the words for each of `a` 240 // and `b` to an appropriate offset in the print buffer, 241 // without having to worry about other threads inserting 242 // print commands between them. 243 // 244 a.writePrintWords(gPrintBuffer, wordOffset); wordOffset += aCount; 245 b.writePrintWords(gPrintBuffer, wordOffset); wordOffset += bCount; 246} 247 248// We can then continue to build up to `println()` functions with 249// three or more arguments. 250 251void println<A : IPrintable, B : IPrintable, C : IPrintable>( 252 A a, B b, C c) 253{ 254 uint wordCount = 0; 255 uint aCount = a.getPrintWordCount(); wordCount += aCount; 256 uint bCount = b.getPrintWordCount(); wordCount += bCount; 257 uint cCount = c.getPrintWordCount(); wordCount += cCount; 258 259 uint wordOffset = _beginPrintln(wordCount); 260 261 a.writePrintWords(gPrintBuffer, wordOffset); wordOffset += aCount; 262 b.writePrintWords(gPrintBuffer, wordOffset); wordOffset += bCount; 263 c.writePrintWords(gPrintBuffer, wordOffset); wordOffset += cCount; 264} 265 266// Further generalizing to four or more arguments is straightforward but tedious. 267// 268// A future version of Slang may support variadic functions, variadic generics, 269// or some other facilities to make writing code like this easier. 270 271// An important benefit of the approach we have taken here with an `IPrintable` 272// interface is that arbitrary user-defined types can implement `IPrintable` 273// and will work correctly with the existing `println()` definitions in 274// this file. 275 276// C-style `printf()` 277// ================== 278// 279// Many developers who use C/C++ would prefer to be able to use traditional 280// `printf()` with format strings. `printf`-based printing tends to be 281// more readable than `println`-style alternatives, but comes at the cost 282// of only supported a more restricted set of types for printing. 283// 284// Similar to the `println()` case, our Slang implementation of `printf()` 285// starts with an allocation function that does the behind-the-scenes 286// work. 287// 288// Note: We use the name `printf_` here because `printf` clashes with 289// HLSL's printf. 290// 291 292uint _beginPrintf(String format, uint wordCount) 293{ 294 // A printf command will start with the usual command header word, 295 // along with a word for the (hashed) format string. These 296 // two header words will be followed by the user-provided payload 297 // words for all the format arguments. 298 // 299 uint wordOffset = _allocatePrintWords(wordCount + 2); 300 gPrintBuffer[wordOffset++] = (uint(PrintingOp.PrintF) << 16) | (wordCount+1); 301 gPrintBuffer[wordOffset++] = getStringHash(format); 302 return wordOffset; 303} 304 305// Now we will define an interface for types that are allowed to 306// appear as format arguments to `printf()`. 307 308interface IPrintf 309{ 310 // A `printf()` format argument must know how many words it encodes into 311 uint getPrintfWordCount(); 312 313 // A `printf()` format argument must know how to encode itself 314 void writePrintfWords(RWStructuredBuffer<uint> buffer, uint offset); 315}; 316 317// The extension to make `uint` compatible with `printf()` is straightforward. 318 319extension uint : IPrintf 320{ 321 // A `uint` only consumes one word in the variadic payload. 322 // 323 // Note: unlike the case for `IPrintable` above, the encoding 324 // for format args for `printf()` doesn't include type information. 325 // 326 uint getPrintfWordCount() { return 1; } 327 328 // Writing the required data to the payload for `printf()` is simple 329 void writePrintfWords(RWStructuredBuffer<uint> buffer, uint offset) 330 { 331 buffer[offset++] = this; 332 } 333} 334 335extension String : IPrintf 336{ 337 uint getPrintfWordCount() { return 1; } 338 339 void writePrintfWords(RWStructuredBuffer<uint> buffer, uint offset) 340 { 341 buffer[offset++] = getStringHash(this); 342 } 343} 344 345 346// A `printf()` with no format arguments can just call back to `_beginPrintf()` 347void printf_(String format) 348{ 349 _beginPrintf(format, 0); 350} 351 352// The `printf()` cases with one or more format arguments are all quite similar. 353 354void printf_<A : IPrintf>(String format, A a) 355{ 356 // We need to compute the words required by each format argument 357 // and sum them up. 358 // 359 uint wordCount = 0; 360 uint aCount = a.getPrintfWordCount(); wordCount += aCount; 361 362 // We need to allocate a `printf()` command in the buffer with 363 // the required number of words for format argument payload. 364 // 365 uint wordOffset = _beginPrintf(format, wordCount); 366 367 // We need to write each format argument to the appropriate offset 368 // in the payload part of the `printf()` command. 369 // 370 a.writePrintfWords(gPrintBuffer, wordOffset); wordOffset += aCount; 371} 372 373void printf_<A : IPrintf, B : IPrintf>(String format, A a, B b) 374{ 375 uint wordCount = 0; 376 uint aCount = a.getPrintfWordCount(); wordCount += aCount; 377 uint bCount = b.getPrintfWordCount(); wordCount += bCount; 378 379 uint wordOffset = _beginPrintf(format, wordCount); 380 381 a.writePrintfWords(gPrintBuffer, wordOffset); wordOffset += aCount; 382 b.writePrintfWords(gPrintBuffer, wordOffset); wordOffset += bCount; 383} 384 385// Extending this `printf()` implementation to handle more format arguments 386// is straightforward, but tedious. Future versions of Slang might add 387// support for variadic generics, which could make this code more compact.