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