yum-mirror/slang

Making it easier to work with shaders

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

Matthew MoultonImprove documentation and example formatting consistency (#4299)78d34f3b3

master
12.8 KiB387 linesraw
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.