1 module example;
2 
3 import gfx.core.rc;
4 import gfx.graal;
5 import gfx.graal.cmd;
6 import gfx.graal.device;
7 import gfx.graal.image;
8 import gfx.graal.queue;
9 import gfx.graal.sync;
10 import gfx.vulkan;
11 import gfx.window;
12 
13 import std.algorithm;
14 import std.exception;
15 import std.typecons;
16 import std.traits : isArray;
17 
18 struct FPSProbe {
19     import std.datetime.stopwatch : StopWatch;
20 
21     private StopWatch sw;
22     private size_t lastUsecs;
23     private size_t lastFc;
24     size_t frameCount;
25 
26     void start() { sw.start(); }
27     void tick() { frameCount += 1; }
28     @property float fps() {
29         const usecs = sw.peek().total!"usecs"();
30         const fps = 1000_000f * (frameCount - lastFc) / (usecs-lastUsecs);
31         lastFc = frameCount;
32         lastUsecs = usecs;
33         return fps;
34     }
35 }
36 
37 class Example : Disposable
38 {
39     string title;
40     string[] args;
41     Rc!Display display;
42     Window window;
43     Rc!Instance instance;
44     uint graphicsQueueIndex;
45     uint presentQueueIndex;
46     Rc!PhysicalDevice physicalDevice;
47     Rc!Device device;
48     Queue graphicsQueue;
49     Queue presentQueue;
50     uint[2] surfaceSize;
51     bool hasAlpha;
52     Rc!Swapchain swapchain;
53     ImageBase[] scImages;
54     Rc!Semaphore imageAvailableSem;
55     Rc!Semaphore renderingFinishSem;
56     Rc!CommandPool cmdPool;
57     CommandBuffer[] cmdBufs;
58     Fence[] fences;
59 
60     enum numCmdBufs=2;
61     size_t cmdBufInd;
62 
63     this (string title, string[] args=[])
64     {
65         this.title = title;
66         this.args = args;
67     }
68 
69     override void dispose() {
70         if (device) {
71             device.waitIdle();
72         }
73         releaseArray(fences);
74         if (cmdPool && cmdBufs.length) {
75             cmdPool.free(cmdBufs);
76             cmdPool.unload();
77         }
78         // the rest is checked with Rc, so it is safe to call unload even
79         // if object is invalid
80         imageAvailableSem.unload();
81         renderingFinishSem.unload();
82         swapchain.unload();
83         device.unload();
84         physicalDevice.unload();
85         if (window) {
86             //window.close();
87         }
88         instance.unload();
89         display.unload();
90     }
91 
92     void prepare()
93     {
94         bool noVulkan = false;
95         foreach (a; args) {
96             if (a == "--no-vulkan" || a == "nv") {
97                 noVulkan = true;
98                 break;
99             }
100         }
101         Backend[] backendLoadOrder;
102         if (!noVulkan) {
103             backendLoadOrder ~= Backend.vulkan;
104         }
105         backendLoadOrder ~= Backend.gl3;
106         // Create a display for the running platform
107         // The instance is created by the display. Backend is chosen at runtime
108         // depending on detected API support. (i.e. Vulkan is preferred)
109         display = createDisplay(backendLoadOrder);
110         instance = display.instance;
111         // Create a window. The surface is created during the call to show.
112         window = display.createWindow();
113         window.show(640, 480);
114 
115         // instance.setDebugCallback((Severity sev, string msg) {
116         //     import std.stdio : writefln;
117         //     writefln("Gfx backend %s message: %s", sev, msg);
118         //     if (sev == Severity.error) {
119         //         // debug break;
120         //         asm { int 0x03; }
121         //     }
122         // });
123 
124         // The rest of the preparation.
125         prepareDevice();
126         prepareSwapchain(null);
127         prepareSync();
128         prepareCmds();
129     }
130 
131     void prepareDevice()
132     {
133         bool checkDevice(PhysicalDevice dev) {
134             graphicsQueueIndex = uint.max;
135             presentQueueIndex = uint.max;
136             if (dev.softwareRendering) return false;
137             foreach (uint i, qf; dev.queueFamilies) {
138                 const graphics = qf.cap & QueueCap.graphics;
139                 const present = dev.supportsSurface(i, window.surface);
140                 if (graphics && present) {
141                     graphicsQueueIndex = i;
142                     presentQueueIndex = i;
143                     return true;
144                 }
145                 if (graphics) graphicsQueueIndex = i;
146                 if (present) presentQueueIndex = i;
147             }
148             return graphicsQueueIndex != uint.max && presentQueueIndex != uint.max;
149         }
150         foreach (pd; instance.devices) {
151             if (checkDevice(pd)) {
152                 auto qrs = [ QueueRequest(graphicsQueueIndex, [ 0.5f ]) ];
153                 if (graphicsQueueIndex != presentQueueIndex) {
154                     qrs ~= QueueRequest(presentQueueIndex, [ 0.5f ]);
155                 }
156                 physicalDevice = pd;
157                 device = pd.open(qrs);
158                 graphicsQueue = device.getQueue(graphicsQueueIndex, 0);
159                 presentQueue = device.getQueue(presentQueueIndex, 0);
160                 break;
161             }
162         }
163     }
164 
165     void prepareSwapchain(Swapchain former=null) {
166         const surfCaps = physicalDevice.surfaceCaps(window.surface);
167         enforce(surfCaps.usage & ImageUsage.transferDst, "TransferDst not supported by surface");
168         const usage = ImageUsage.colorAttachment;
169         const numImages = max(2, surfCaps.minImages);
170         enforce(surfCaps.maxImages == 0 || surfCaps.maxImages >= numImages);
171         const f = chooseFormat(physicalDevice, window.surface);
172 
173         CompositeAlpha ca;
174         if (surfCaps.supportedAlpha & CompositeAlpha.preMultiplied) {
175             ca = CompositeAlpha.preMultiplied;
176         }
177         else if (surfCaps.supportedAlpha & CompositeAlpha.inherit) {
178             ca = CompositeAlpha.inherit;
179         }
180         else if (surfCaps.supportedAlpha & CompositeAlpha.postMultiplied) {
181             ca = CompositeAlpha.postMultiplied;
182         }
183         else {
184             ca = CompositeAlpha.opaque;
185         }
186         hasAlpha = ca != CompositeAlpha.opaque;
187 
188         surfaceSize = [ 640, 480 ];
189         foreach (i; 0..2) {
190             surfaceSize[i] = clamp(surfaceSize[i], surfCaps.minSize[i], surfCaps.maxSize[i]);
191         }
192         const pm = choosePresentMode(physicalDevice, window.surface);
193 
194         swapchain = device.createSwapchain(window.surface, pm, numImages, f, surfaceSize, usage, ca, former);
195         scImages = swapchain.images;
196     }
197 
198     void prepareSync() {
199         imageAvailableSem = device.createSemaphore();
200         renderingFinishSem = device.createSemaphore();
201         fences = new Fence[numCmdBufs];
202         foreach (i; 0 .. numCmdBufs) {
203             fences[i] = device.createFence(Yes.signaled);
204         }
205         retainArray(fences);
206     }
207 
208     void prepareCmds() {
209         cmdPool = device.createCommandPool(graphicsQueueIndex);
210         cmdBufs = cmdPool.allocate(numCmdBufs);
211     }
212 
213     abstract void recordCmds(size_t cmdBufInd, size_t imgInd);
214 
215     size_t nextCmdBuf() {
216         const ind = cmdBufInd++;
217         if (cmdBufInd == numCmdBufs) {
218             cmdBufInd = 0;
219         }
220         return ind;
221     }
222 
223     void render()
224     {
225         import core.time : dur;
226 
227         bool needReconstruction;
228         const imgInd = swapchain.acquireNextImage(dur!"seconds"(-1), imageAvailableSem, needReconstruction);
229         const cmdBufInd = nextCmdBuf();
230 
231         fences[cmdBufInd].wait();
232         fences[cmdBufInd].reset();
233 
234         recordCmds(cmdBufInd, imgInd);
235 
236         submit(cmdBufInd);
237 
238         presentQueue.present(
239             [ renderingFinishSem ],
240             [ PresentRequest(swapchain, imgInd) ]
241         );
242 
243         // if (needReconstruction) {
244         //     prepareSwapchain(swapchain);
245         //     presentPool.reset();
246         // }
247     }
248 
249     void submit(ulong cmdBufInd) {
250         graphicsQueue.submit([
251             Submission (
252                 [ StageWait(imageAvailableSem, PipelineStage.transfer) ],
253                 [ renderingFinishSem.obj ], [ cmdBufs[cmdBufInd] ]
254             )
255         ], fences[cmdBufInd] );
256     }
257 
258 
259     // Following functions are general utility that can be used by subclassing
260     // examples.
261 
262     /// Find a format supported by the device for the given tiling and features
263     Format findSupportedFormat(in Format[] candidates, in ImageTiling tiling, in FormatFeatures features)
264     {
265         foreach (f; candidates) {
266             const props = physicalDevice.formatProperties(f);
267             if (tiling == ImageTiling.optimal &&
268                     (props.optimalTiling & features) == features) {
269                 return f;
270             }
271             if (tiling == ImageTiling.linear &&
272                     (props.linearTiling & features) == features) {
273                 return f;
274             }
275         }
276         throw new Exception("could not find supported format");
277     }
278 
279     /// Find a supported depth format
280     Format findDepthFormat() {
281         return findSupportedFormat(
282             [ Format.d32_sFloat, Format.d32s8_sFloat, Format.d24s8_uNorm, Format.d16_uNorm, Format.d16s8_uNorm ],
283             ImageTiling.optimal, FormatFeatures.depthStencilAttachment
284         );
285     }
286 
287     /// Return the index of a memory type supporting all of props,
288     /// or uint.max if none was found.
289     uint findMemType(MemoryRequirements mr, MemProps props)
290     {
291         const devMemProps = physicalDevice.memoryProperties;
292         foreach (i, mt; devMemProps.types) {
293             if ((mr.memTypeMask & (1 << i)) != 0 && (mt.props & props) == props) {
294                 return cast(uint)i;
295             }
296         }
297         return uint.max;
298     }
299 
300 
301     /// Create a buffer for usage, bind memory of dataSize with memProps
302     /// Return null if no memory type can be found
303     final Buffer createBuffer(size_t dataSize, BufferUsage usage, MemProps props)
304     {
305         auto buf = device.createBuffer( usage, dataSize ).rc;
306 
307         const mr = buf.memoryRequirements;
308         const memTypeInd = findMemType(mr, props);
309         if (memTypeInd == uint.max) return null;
310 
311         auto mem = device.allocateMemory(memTypeInd, mr.size).rc;
312         buf.bindMemory(mem, 0);
313 
314         return buf.giveAway();
315     }
316 
317     /// Create a buffer, binds memory to it, and leave content undefined
318     /// The buffer will be host visible and host coherent such as content
319     /// can be updated without staging buffer
320     final Buffer createDynamicBuffer(size_t dataSize, BufferUsage usage)
321     {
322         return createBuffer(dataSize, usage, MemProps.hostVisible | MemProps.hostCoherent);
323     }
324 
325     /// Create a buffer, and bind it with memory filled with data.
326     /// The bound memory will be deviceLocal, without guarantee to be host visible.
327     final Buffer createStaticBuffer(const(void)[] data, BufferUsage usage)
328     {
329         const dataSize = data.length;
330 
331         // On embedded gpus, device local memory is often also host visible.
332         // Attempting to create one that way.
333         if (physicalDevice.type != DeviceType.discreteGpu) {
334             auto buf = createBuffer(
335                 dataSize, usage,
336                 MemProps.hostVisible | MemProps.hostCoherent | MemProps.deviceLocal
337             ).rc;
338             if (buf) {
339                 auto mm = buf.boundMemory.map(0, dataSize);
340                 mm[] = data;
341                 return buf.giveAway();
342             }
343         }
344 
345         // did not happen :-(
346         // will go the usual way: staging buffer then device local buffer
347 
348         // create staging buffer
349         auto stagingBuf = enforce(createBuffer(
350             dataSize, BufferUsage.transferSrc, MemProps.hostVisible | MemProps.hostCoherent
351         )).rc;
352 
353         // populate data
354         {
355             auto mm = stagingBuf.boundMemory.map(0, dataSize);
356             mm[] = data;
357         }
358 
359         // create actual buffer
360         auto buf = enforce(createBuffer(
361             dataSize, usage | BufferUsage.transferDst, MemProps.deviceLocal
362         )).rc;
363 
364         auto b = autoCmdBuf().rc;
365 
366         // copy from staging buffer
367         copyBuffer(stagingBuf, buf, dataSize, b.cmdBuf);
368 
369         // return data
370         return buf.giveAway();
371     }
372 
373     /// ditto
374     final Buffer createStaticBuffer(T)(const(T)[] data, BufferUsage usage)
375     if (!is(T == void))
376     {
377         return createStaticBuffer(untypeSlice(data), usage);
378     }
379 
380     /// ditto
381     final Buffer createStaticBuffer(T)(in T data, BufferUsage usage)
382     if (!isArray!T)
383     {
384         const start = cast(const(void)*)&data;
385         return createStaticBuffer(start[0 .. data.sizeof], usage);
386     }
387 
388     bool bindImageMemory(Image img, MemProps props=MemProps.deviceLocal) {
389         const mr = img.memoryRequirements;
390         const memTypeInd = findMemType(mr, MemProps.deviceLocal);
391         if (memTypeInd == uint.max) return false;
392 
393         auto mem = device.allocateMemory(memTypeInd, mr.size).rc;
394         img.bindMemory(mem, 0);
395         return true;
396     }
397 
398     /// create an image to be used as texture
399     Image createTextureImage(const(void)[] data, in ImageInfo info)
400     {
401         const FormatFeatures requirement = FormatFeatures.sampledImage;
402         const formatProps = physicalDevice.formatProperties(info.format);
403         enforce( (formatProps.optimalTiling & requirement) == requirement );
404 
405         // create staging buffer
406         auto stagingBuf = enforce(createBuffer(
407             data.length, BufferUsage.transferSrc, MemProps.hostVisible | MemProps.hostCoherent
408         )).rc;
409 
410         // populate data to buffer
411         {
412             auto mm = stagingBuf.boundMemory.map(0, data.length);
413             mm[] = data;
414         }
415 
416         // create an image
417         auto img = enforce(device.createImage(
418             info.withUsage(info.usage | ImageUsage.sampled | ImageUsage.transferDst)
419         )).rc;
420 
421         // allocate memory image
422         if (!bindImageMemory(img)) return null;
423 
424         {
425             auto b = autoCmdBuf().rc;
426 
427             b.cmdBuf.pipelineBarrier(
428                 trans(PipelineStage.topOfPipe, PipelineStage.transfer), [], [
429                     ImageMemoryBarrier(
430                         trans(Access.none, Access.transferWrite),
431                         trans(ImageLayout.undefined, ImageLayout.transferDstOptimal),
432                         trans(queueFamilyIgnored, queueFamilyIgnored),
433                         img, ImageSubresourceRange(ImageAspect.color)
434                     )
435                 ]
436             );
437             copyBufferToImage(stagingBuf, img, b.cmdBuf);
438         }
439 
440         return img.giveAway();
441     }
442 
443     /// Create an image for depth attachment usage
444     Image createDepthImage(uint width, uint height)
445     {
446         // find the format of the image
447         const f = findDepthFormat();
448 
449         // create an image
450         auto img = enforce(device.createImage(
451             ImageInfo.d2(width, height).withFormat(f).withUsage(ImageUsage.depthStencilAttachment)
452         )).rc;
453 
454         // allocate memory image
455         if (!bindImageMemory(img)) return null;
456 
457         return img.giveAway();
458     }
459 
460     /// Create an image for stencil attachment usage
461     Image createStencilImage(uint width, uint height)
462     {
463         // assume s8_uInt is supported
464         const f = Format.s8_uInt;
465 
466         // create an image
467         auto img = enforce(device.createImage(
468             ImageInfo.d2(width, height).withFormat(f).withUsage(ImageUsage.depthStencilAttachment)
469         )).rc;
470 
471         // allocate memory image
472         if (!bindImageMemory(img)) return null;
473 
474         return img.giveAway();
475     }
476 
477     /// copy the content of one buffer to another
478     /// srcBuf and dstBuf must support transferSrc and transferDst respectively.
479     final void copyBuffer(Buffer srcBuf, Buffer dstBuf, size_t size, CommandBuffer cmdBuf)
480     {
481         cmdBuf.copyBuffer(trans(srcBuf, dstBuf), [CopyRegion(trans!size_t(0, 0), size)]);
482     }
483 
484     /// copy the content of one buffer to an image.
485     /// the image layout must be transferDstOptimal buffer the call
486     final void copyBufferToImage(Buffer srcBuf, Image dstImg, CommandBuffer cmdBuf)
487     {
488         const dims = dstImg.info.dims;
489 
490         BufferImageCopy region;
491         region.extent = [dims.width, dims.height, dims.depth];
492         const regions = (&region)[0 .. 1];
493         cmdBuf.copyBufferToImage(srcBuf, dstImg, ImageLayout.transferDstOptimal, regions);
494     }
495 
496     /// Get a RAII command buffer that is meant to be trashed after usage.
497     /// Returned buffer is ready to record data, and execute commands on the graphics queue
498     /// at end of scope.
499     AutoCmdBuf autoCmdBuf(CommandPool pool=null)
500     {
501         Rc!CommandPool cmdPool = pool;
502         if (!cmdPool) {
503             cmdPool = enforce(this.cmdPool);
504         }
505         return new AutoCmdBuf(cmdPool, graphicsQueue);
506     }
507 }
508 
509 /// Utility command buffer for a one time submission that automatically submit
510 /// when disposed.
511 /// Generally used for transfer operations.
512 class AutoCmdBuf : AtomicRefCounted
513 {
514     mixin(atomicRcCode);
515 
516     Rc!CommandPool pool;
517     Queue queue;
518     Rc!Device device; // device holds queue,
519     CommandBuffer cmdBuf;
520 
521     this(CommandPool pool, Queue queue) {
522         this.pool = pool;
523         this.queue = queue;
524         this.device = queue.device;
525         this.cmdBuf = this.pool.allocate(1)[0];
526         this.cmdBuf.begin(No.persistent);
527     }
528 
529     override void dispose() {
530         this.cmdBuf.end();
531         this.queue.submit([
532             Submission([], [], [ this.cmdBuf ])
533         ], null);
534         this.queue.waitIdle();
535         this.pool.free([ this.cmdBuf ]);
536         this.pool.unload();
537         this.device.unload();
538     }
539 
540 }
541 
542 /// Return a format suitable for the surface.
543 ///  - if supported by the surface Format.rgba8_uNorm
544 ///  - otherwise the first format with uNorm numeric format
545 ///  - otherwise the first format
546 Format chooseFormat(PhysicalDevice pd, Surface surface)
547 {
548     const formats = pd.surfaceFormats(surface);
549     enforce(formats.length, "Could not get surface formats");
550     if (formats.length == 1 && formats[0] == Format.undefined) {
551         return Format.rgba8_uNorm;
552     }
553     foreach(f; formats) {
554         if (f == Format.rgba8_uNorm) {
555             return f;
556         }
557     }
558     foreach(f; formats) {
559         if (f.formatDesc.numFormat == NumFormat.uNorm) {
560             return f;
561         }
562     }
563     return formats[0];
564 }
565 
566 PresentMode choosePresentMode(PhysicalDevice pd, Surface surface)
567 {
568     // auto modes = pd.surfacePresentModes(surface);
569     // if (modes.canFind(PresentMode.mailbox)) {
570     //     return PresentMode.mailbox;
571     // }
572     assert(pd.surfacePresentModes(surface).canFind(PresentMode.fifo));
573     return PresentMode.fifo;
574 }