yum-mirror/slang
Making it easier to work with shaders
git clone https://git.yummers.dev/yum-mirror/slang
49667272a
master
1// gpu-printing.cpp 2#include "gpu-printing.h" 3 4#include <assert.h> 5#include <string.h> 6 7// This file implements the CPU side of a simple GPU printing 8// library. The CPU code is responsible for scanning through 9// buffers of "print commands" produced by GPU shaders, and 10// executing those commands to print output. 11// 12// The opcodes for the printing commands are shared between 13// CPU and GPU, and also between C++ and Slang, by putting 14// their declarations in the `gpu-printing-ops.h` file 15// and including them into both the host and device code 16// to generate `enum` types. 17// 18enum class GPUPrintingOp :uint32_t 19{ 20#define GPU_PRINTING_OP(NAME) NAME, 21#include "gpu-printing-ops.h" 22}; 23 24// One of the key ideas in this printing system is that strings 25// are not encoded into the buffer of print commands directly, 26// but are instead encoded using a hash of the string data. 27// 28// In order to map from a hash code back to the original string, 29// the host side code for the printing system needs a way to 30// pre-populate a lookup table with the strings that appear 31// in a shader. The Slang reflection API provides a service to 32// do exactly that. 33// 34void GPUPrinting ::loadStrings (slang::ProgramLayout * slangReflection ) 35{ 36// Given the Slang-generated reflection and layout information 37// for a program, we can query the number of string literals 38// that appear in the linked program. 39// 40SlangUInt hashedStringCount = slangReflection -> getHashedStringCount (); 41for (SlangUInt ii = 0 ;ii < hashedStringCount ;++ ii ) 42 { 43// For each string we can fetch its bytes from the Slang 44// reflection data. 45// 46size_t stringSize = 0 ; 47char const * stringData = slangReflection -> getHashedString (ii ,& stringSize ); 48 49// Then we can compute the hash code for that string using 50// another Slang API function. 51// 52// Note: the exact hashing algorithm that Slang uses for 53// string literals is not currently documented, and may 54// change in future releases of the compiler. 55// 56StringHash hash = spComputeStringHash (stringData ,stringSize ); 57 58// The `GPUPrinting` implementation will store the mapping 59// from hash codes back to strings in a simple STL `map`. 60// 61m_hashedStrings .insert ( 62 std::make_pair (hash , std::string (stringData ,stringData + stringSize ))); 63 } 64} 65 66// The main service that the host code for the GPU printing library 67// provides is a way to execute the printing commands that have been 68// encoded to a buffer by shader code. 69// 70void GPUPrinting ::processGPUPrintCommands (const void * data ,size_t dataSize ) 71{ 72// Everything that the GPU code writes to the buffer will be in 73// a granularity of 32-bits words, so we start by computing 74// how many words, total, will fit in the buffer. 75// 76uint32_t dataWordCount = uint32_t (dataSize /sizeof (uint32_t )); 77// 78// If the buffer doesn't even have enough space for the leading counter, 79// then there is nothing to print. 80// 81if (dataWordCount < 1 ) 82 { 83fprintf (stderr ,"error: expected at least 4 bytes in GPU printing buffer\n" ); 84return ; 85 } 86// 87// Otherwise, we set ourselves up to start reading data from the buffer 88// at a granularity of 32-bit words. 89// 90const uint32_t * dataCursor = (const uint32_t * )data ; 91 92// The first word of a printing buffer gives us the total number of 93// words that were appended by GPU printing operations. 94// 95uint32_t wordsAppended = * dataCursor ++ ; 96// 97// Under normal operation, we will stop processing data from 98// the buffer after we have read everything the GPU wrote. 99// 100const uint32_t * dataEnd = dataCursor + wordsAppended ; 101 102// If the number of bytes the GPU code tried to write (including 103// the counter stored in the first word of the buffer) exceeds what 104// the buffer could hold, then we will print a warning message, 105// indicating that the application might want to allocate a 106// larger buffer. 107// 108size_t totalBytesWritten = sizeof (uint32_t )* (wordsAppended + 1 ); 109if (totalBytesWritten > dataSize ) 110 { 111fprintf ( 112stderr , 113"warning: GPU code attempted to write %llu bytes to the printing buffer, but only %llu " 114"bytes were available\n" , 115 (unsigned long long )totalBytesWritten , 116 (unsigned long long )dataSize ); 117 118// If the buffer is full, then we only want to read through 119// to the end of what is available. 120// 121dataEnd = ((const uint32_t * )data )+ dataWordCount ; 122 } 123 124// We will now proceed to read off "commands" from the buffer, 125// and execute those commands to print things to `stdout`. 126// 127while (dataCursor < dataEnd ) 128 { 129// The first word of each command is encoded to hold both 130// an "opcode" for the command, and the number of "payload" 131// words that follow the header. 132// 133uint32_t cmdHeader = * dataCursor ++ ; 134GPUPrintingOp op = GPUPrintingOp ((cmdHeader >>16 )& 0xFFFF ); 135uint32_t payloadWordCount = cmdHeader & 0xFFFF ; 136 137// It is possible that we are at the end of the buffer, 138// and not all of the payload words could be written. 139// In such a case we will bail out of the printing loop to 140// avoid crashes from a command trying to fetch data past 141// the end of the buffer. 142// 143ptrdiff_t wordsAvailable = dataEnd - dataCursor ; 144if (wordsAvailable < 0 || payloadWordCount > (uint32_t )wordsAvailable ) 145 { 146fprintf ( 147stderr , 148"error: GPU printing buffer corruption or insufficient data for payload.\n" 149" Op: %d, Declared Payload Words: %u, Available Words: %td\n" , 150 (int )op , 151payloadWordCount , 152 (wordsAvailable < 0 ?0 :wordsAvailable )); 153break ; 154 } 155// 156// Otherwise, we can form a pointer to the payload words 157// for this command, and advance our cursor past the payload 158// to set up for reading the next command. 159// 160const uint32_t * payloadWords = dataCursor ; 161const uint32_t * payloadWordsEnd = payloadWords + payloadWordCount ; 162dataCursor += payloadWordCount ; 163 164// What to do with a command depends a lot on which "op" was selected. 165switch (op ) 166 { 167default : 168// If we encounter an op that we don't understand, there is a change 169// that the buffer is corrupted or invalid, but we will try to 170// soldier on and process further commands. 171// 172fprintf (stderr ,"error: unexpected GPU printing op %d\n" , (int )op ); 173break ; 174 175case GPUPrintingOp ::Nop : 176// The `Nop` case is a no-op, and allows GPU code to conservatively 177// allocate bytes in the printing buffer and then overwrite any 178// excess with zeros to trim their allocation. 179break ; 180 181case GPUPrintingOp ::NewLine : 182// The `NewLine` case prints a single '\n' and doesn't need any payload. 183putchar ('\n' ); 184break ; 185 186// Simple value printing cases can just load the bytes of 187// a value directly from the payload, and then print it. 188// 189// We will use a macro to avoid duplication the code shared 190// between these cases. 191// 192#define CASE (OP ,FORMAT ,TYPE ) \ 193 case GPUPrintingOp::OP: \ 194 { \ 195 TYPE value; \ 196 assert(payloadWordCount >= (sizeof(value) / sizeof(uint32_t))); \ 197 memcpy(&value, payloadWords, sizeof(value)); \ 198 printf(FORMAT, value); \ 199 } \ 200 break 201 202CASE (Int32 ,"%d" ,int ); 203CASE (UInt32 ,"%u" ,unsigned int ); 204CASE (Float32 ,"%f" ,float ); 205 206#undef CASE 207 208case GPUPrintingOp ::String : 209 { 210// Strings are handled differently than other values because 211// most GPU graphics APIs do not natively support strings 212// in shader code. 213// 214// Instead, strings are handled by the printing logic in 215// terms of 32-bit hash codes. When printing a string, 216// the generated GPU code will write the hash value for 217// the string to the print buffer. 218// 219// On the CPU, we then read the hash code from the payload 220// for this command: 221// 222assert (payloadWordCount >=1 ); 223StringHash hash = * payloadWords ++ ; 224// 225// Next, we look up the hash value in a map from hash 226// codes to strings, that was seeded with strings known 227// to appear in the GPU code. 228// 229auto iter = m_hashedStrings .find (hash ); 230if (iter == m_hashedStrings .end ()) 231 { 232// If we didn't have a string to match that hash code in 233// our map, we can continue trying to print, but it is 234// likely that the application code needs to be configured 235// to pass in the right strings. 236// 237fprintf (stderr ,"error: string with unknown hash 0x%x\n" ,hash ); 238continue ; 239 } 240 241// Once we've found a string that matches our hash 242// code, we can print it. 243// 244// TODO: This code isn't robust against strings with 245// embeded null bytes. 246// s 247printf ("%s" ,iter -> second .c_str ()); 248 } 249break ; 250 251case GPUPrintingOp ::PrintF : 252 { 253// Handling a general-purpose `printf` call requires looking 254// up the format string, and then processing further payload 255// words based on the format. 256// 257// Finding the format string follows logic similar to the 258// `GPUPrintingOp::String` case. 259// 260assert (payloadWords != payloadWordsEnd ); 261StringHash formatHash = * payloadWords ++ ; 262 263auto iter = m_hashedStrings .find (formatHash ); 264if (iter == m_hashedStrings .end ()) 265 { 266// If we didn't have a string to match that hash code in 267// our map, we can continue trying to print, but it is 268// likely that the application code needs to be configured 269// to pass in the right strings. 270// 271fprintf (stderr ,"error: string with unknown hash 0x%x\n" ,formatHash ); 272continue ; 273 } 274 std::string format = iter -> second ; 275 276// We can't just route things through to the `printf()` function 277// provided by standard library on the host CPU, because we don't 278// have a portable way to translate the payload data into 279// varargs that match the platform ABI. 280// 281// Instead, we have to scan through the string ourselves, and 282// implement a subset of the full `printf()`. 283// 284const char * cursor = format .c_str (); 285const char * end = cursor + format .length (); 286while (cursor != end ) 287 { 288int c = * cursor ++ ; 289 290// If we see a byte other than `%`, then we can just 291// output it directly and keep scanning the format string. 292// 293if (c != '%' ) 294 { 295putchar (c ); 296continue ; 297 } 298 299// Otherwise, we have a `%` which is supposed to 300// introduce a format specifier. 301// 302// If we are somehow at the end of the format 303// string, then the format was bad. 304// 305if (cursor == end ) 306 { 307fprintf (stderr ,"error: unexpected '%%' at and of format string\n" ); 308break ; 309 } 310 311// If the next byte in the format string is 312// the `%` character, then it is an escaped 313// `%` so we should just emit it as-is and move along. 314// 315if (* cursor == '%' ) 316 { 317putchar (* cursor ++ ); 318continue ; 319 } 320 321// TODO: For proper `printf()` support, we would need 322// to read: 323// 324// * optional flags: `-+#0` 325// * optional width specifier: a number or `*` 326// * optional precision specifier: `.` and a number or `*` 327// * optional length sub-specifiers: `h`, `l`, `ll`, etc. 328// 329// For now we ignore all those details and just 330// read a single-byte specifier. 331// 332int specifier = * cursor ++ ; 333switch (specifier ) 334 { 335default : 336fprintf ( 337stderr , 338"error: unexpected format specifier '%c' (0x%X)\n" , 339specifier , 340specifier ); 341break ; 342 343 344// When processing each format speecifier, we will 345// read words from the payload, as necessary 346// to yield a value of the expected type. 347// 348// To reduce the amount of boilerplate, we will 349// use a macro to capture the shared code for 350// common cases. 351// 352#define CASE (CHAR ,FORMAT ,TYPE ) \ 353 case CHAR: \ 354 { \ 355 assert(payloadWords != payloadWordsEnd); \ 356 TYPE value; \ 357 memcpy(&value, payloadWords, sizeof(value)); \ 358 payloadWords += sizeof(value) / sizeof(uint32_t); \ 359 printf(FORMAT, value); \ 360 } \ 361 break 362 363case 'i' :// `%i` is just an alias for `%d` 364CASE ('d' ,"%d" ,int ); 365CASE ('u' ,"%u" ,unsigned int ); 366CASE ('x' ,"%x" ,unsigned int ); 367CASE ('X' ,"%X" ,unsigned int ); 368 369// Note: all of our printing support for floating-point 370// values will use the `float` type instead of `double`. 371// This isn't compatible with C rules, but makes more sense 372// for GPU code. 373// 374CASE ('f' ,"%f" ,float ); 375CASE ('F' ,"%F" ,float ); 376CASE ('e' ,"%e" ,float ); 377CASE ('E' ,"%E" ,float ); 378CASE ('g' ,"%g" ,float ); 379CASE ('G' ,"%G" ,float ); 380CASE ('c' ,"%c" ,int ); 381 382#undef CASE 383 384case 's' : 385 { 386// The case for strings is more complicated 387// just because it has to deal with our hashing 388// scheme. 389// 390assert (payloadWords != payloadWordsEnd ); 391StringHash hash = * payloadWords ++ ; 392auto iter = m_hashedStrings .find (hash ); 393if (iter == m_hashedStrings .end ()) 394 { 395fprintf (stderr ,"error: string with unknown hash 0x%x\n" ,hash ); 396continue ; 397 } 398printf ("%s" ,iter -> second .c_str ()); 399 } 400break ; 401 } 402 } 403 } 404break ; 405 } 406 } 407}