yum-mirror/slang

Making it easier to work with shaders

git clone https://git.yummers.dev/yum-mirror/slang

Gangzheng Tongfix the break to make sure only valid data will be accessed (#7148)49667272a

master
16.8 KiB407 linesraw
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    //
40    SlangUInt hashedStringCount = slangReflection->getHashedStringCount();
41    for (SlangUInt ii = 0; ii < hashedStringCount; ++ii)
42    {
43        // For each string we can fetch its bytes from the Slang
44        // reflection data.
45        //
46        size_t stringSize = 0;
47        char 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        //
56        StringHash 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        //
61        m_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    //
76    uint32_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    //
81    if (dataWordCount < 1)
82    {
83        fprintf(stderr, "error: expected at least 4 bytes in GPU printing buffer\n");
84        return;
85    }
86    //
87    // Otherwise, we set ourselves up to start reading data from the buffer
88    // at a granularity of 32-bit words.
89    //
90    const 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    //
95    uint32_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    //
100    const 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    //
108    size_t totalBytesWritten = sizeof(uint32_t) * (wordsAppended + 1);
109    if (totalBytesWritten > dataSize)
110    {
111        fprintf(
112            stderr,
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        //
121        dataEnd = ((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    //
127    while (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        //
133        uint32_t cmdHeader = *dataCursor++;
134        GPUPrintingOp op = GPUPrintingOp((cmdHeader >> 16) & 0xFFFF);
135        uint32_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        //
143        ptrdiff_t wordsAvailable = dataEnd - dataCursor;
144        if (wordsAvailable < 0 || payloadWordCount > (uint32_t)wordsAvailable)
145        {
146            fprintf(
147                stderr,
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,
151                payloadWordCount,
152                (wordsAvailable < 0 ? 0 : wordsAvailable));
153            break;
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        //
160        const uint32_t* payloadWords = dataCursor;
161        const uint32_t* payloadWordsEnd = payloadWords + payloadWordCount;
162        dataCursor += payloadWordCount;
163
164        // What to do with a command depends a lot on which "op" was selected.
165        switch (op)
166        {
167        default:
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            //
172            fprintf(stderr, "error: unexpected GPU printing op %d\n", (int)op);
173            break;
174
175        case 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.
179            break;
180
181        case GPUPrintingOp::NewLine:
182            // The `NewLine` case prints a single '\n' and doesn't need any payload.
183            putchar('\n');
184            break;
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
202            CASE(Int32, "%d", int);
203            CASE(UInt32, "%u", unsigned int);
204            CASE(Float32, "%f", float);
205
206#undef CASE
207
208        case 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                //
222                assert(payloadWordCount >= 1);
223                StringHash 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                //
229                auto iter = m_hashedStrings.find(hash);
230                if (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                    //
237                    fprintf(stderr, "error: string with unknown hash 0x%x\n", hash);
238                    continue;
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
247                printf("%s", iter->second.c_str());
248            }
249            break;
250
251        case 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                //
260                assert(payloadWords != payloadWordsEnd);
261                StringHash formatHash = *payloadWords++;
262
263                auto iter = m_hashedStrings.find(formatHash);
264                if (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                    //
271                    fprintf(stderr, "error: string with unknown hash 0x%x\n", formatHash);
272                    continue;
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                //
284                const char* cursor = format.c_str();
285                const char* end = cursor + format.length();
286                while (cursor != end)
287                {
288                    int 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                    //
293                    if (c != '%')
294                    {
295                        putchar(c);
296                        continue;
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                    //
305                    if (cursor == end)
306                    {
307                        fprintf(stderr, "error: unexpected '%%' at and of format string\n");
308                        break;
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                    //
315                    if (*cursor == '%')
316                    {
317                        putchar(*cursor++);
318                        continue;
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                    //
332                    int specifier = *cursor++;
333                    switch (specifier)
334                    {
335                    default:
336                        fprintf(
337                            stderr,
338                            "error: unexpected format specifier '%c' (0x%X)\n",
339                            specifier,
340                            specifier);
341                        break;
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
363                    case 'i': // `%i` is just an alias for `%d`
364                        CASE('d', "%d", int);
365                        CASE('u', "%u", unsigned int);
366                        CASE('x', "%x", unsigned int);
367                        CASE('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                        //
374                        CASE('f', "%f", float);
375                        CASE('F', "%F", float);
376                        CASE('e', "%e", float);
377                        CASE('E', "%E", float);
378                        CASE('g', "%g", float);
379                        CASE('G', "%G", float);
380                        CASE('c', "%c", int);
381
382#undef CASE
383
384                    case 's':
385                        {
386                            // The case for strings is more complicated
387                            // just because it has to deal with our hashing
388                            // scheme.
389                            //
390                            assert(payloadWords != payloadWordsEnd);
391                            StringHash hash = *payloadWords++;
392                            auto iter = m_hashedStrings.find(hash);
393                            if (iter == m_hashedStrings.end())
394                            {
395                                fprintf(stderr, "error: string with unknown hash 0x%x\n", hash);
396                                continue;
397                            }
398                            printf("%s", iter->second.c_str());
399                        }
400                        break;
401                    }
402                }
403            }
404            break;
405        }
406    }
407}