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