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 = (®ion)[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 }