16#include <SDL2/SDL_vulkan.h>
32#include "common/config.h"
36#include "zeroerr/assert.h"
41#include <assimp/mesh.h>
42#include <assimp/matrix3x3.h>
43#include <assimp/matrix4x4.h>
44#include <assimp/vector3.h>
45#include <glm/gtc/matrix_transform.hpp>
47#include "graphics/shaders/textured_vert_spv.inc"
48#include "graphics/shaders/textured_frag_spv.inc"
49#include "graphics/shaders/mesh3d_vert_spv.inc"
50#include "graphics/shaders/mesh3d_frag_spv.inc"
51#include "graphics/shaders/mesh3d_hair_vert_spv.inc"
52#include "graphics/shaders/mesh3d_hair_frag_spv.inc"
59void transitionSampledColorForTransfer(vk::CommandBuffer cb, vkb::ColorTarget &image,
60 vk::ImageLayout newLayout) {
61 vk::ImageMemoryBarrier barrier{};
62 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
63 barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
64 barrier.oldLayout =
image.currentLayout();
65 barrier.newLayout = newLayout;
66 barrier.image =
image.image();
67 barrier.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
68 vk::PipelineStageFlags srcStages;
69 vk::PipelineStageFlags dstStages;
70 if (newLayout == vk::ImageLayout::eTransferSrcOptimal) {
71 barrier.srcAccessMask = vk::AccessFlagBits::eShaderRead;
72 barrier.dstAccessMask = vk::AccessFlagBits::eTransferRead;
73 srcStages = vk::PipelineStageFlagBits::eVertexShader |
74 vk::PipelineStageFlagBits::eFragmentShader |
75 vk::PipelineStageFlagBits::eComputeShader;
76 dstStages = vk::PipelineStageFlagBits::eTransfer;
78 barrier.srcAccessMask = vk::AccessFlagBits::eTransferRead;
79 barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
80 srcStages = vk::PipelineStageFlagBits::eTransfer;
81 dstStages = vk::PipelineStageFlagBits::eVertexShader |
82 vk::PipelineStageFlagBits::eFragmentShader |
83 vk::PipelineStageFlagBits::eComputeShader;
85 cb.pipelineBarrier(srcStages, dstStages, {}, 0,
nullptr, 0,
nullptr, 1, &barrier);
86 image.setCurrentLayout(newLayout);
97std::vector<uint32_t> loadSpirvBytes(
const void *data,
size_t size) {
98 if (!data ||
size < 4 || (
size % 4) != 0)
100 const auto *words =
static_cast<const uint32_t *
>(data);
101 if (words[0] != 0x07230203)
102 throw Exception(
"SPIR-V: bad magic (expected 0x07230203)");
103 return std::vector<uint32_t>(words, words +
size / 4);
106std::vector<uint32_t> readSpirvFile(
const std::string &
path) {
107 auto *fs = filesystem::Filesystem::create();
108 std::unique_ptr<filesystem::FileData> fd(fs->read(
path));
109 if (!fd)
throw Exception(
"newShaderFromSpvFile: failed to read '%s'",
path.c_str());
110 return loadSpirvBytes(fd->getData(), fd->getSize());
117std::vector<uint32_t> compileShaderStage(
const std::string &
source,
const char *stage,
GlslStage glslStage) {
118 if (
source.empty())
throw Exception(
"newShader: empty %s GLSL", stage);
121 }
catch (
const std::exception &
error) {
124 throw Exception(
"newShader: GLSL compile failed for %s:\n%s", stage,
error.what());
130void Graphics::ensurePresentCaptureHook() {
136 presentModel.after_render_before_present =
nullptr;
139void Graphics::ensureReadbackSlots() {
142 const size_t want = std::max<size_t>(2, frameSlotCount());
143 if (!screenReadbackSlots.empty() && screenReadbackSlots.size() >= want &&
144 screenReadbackBytes ==
bytes)
149 for (
auto &slot : screenReadbackSlots) {
151 slot.staging.unmap();
152 slot.mapped =
nullptr;
154 slot.staging.release();
156 screenReadbackSlots.clear();
157 screenReadbackSlots.resize(want);
158 for (
auto &slot : screenReadbackSlots) {
159 slot.staging = vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eTransferDst,
160 vk::DeviceSize(
bytes),
161 vk::MemoryPropertyFlagBits::eHostVisible |
162 vk::MemoryPropertyFlagBits::eHostCoherent);
164 screenReadbackBytes =
bytes;
165 readbackReady =
false;
166 readbackCpuSynced =
false;
167 readbackWriteSlot = 0;
170bool Graphics::recordSwapchainReadback(vk::CommandBuffer cb) {
172 if (!presentModel.has_acquired_image)
return false;
173 const vk::Format fmt = swapchain.image_format;
174 const bool bgra = (fmt == vk::Format::eB8G8R8A8Unorm || fmt == vk::Format::eB8G8R8A8Srgb);
175 const bool rgba = (fmt == vk::Format::eR8G8B8A8Unorm || fmt == vk::Format::eR8G8B8A8Srgb);
176 if (!bgra && !rgba)
return false;
179 const uint32_t imageIndex = presentModel.acquired_image_index;
180 auto &images = swapchain.get_images();
181 if (imageIndex >= images.size())
return false;
183 ensureReadbackSlots();
184 if (screenReadbackSlots.empty())
return false;
186 const size_t slot = size_t(presentRecording.slot().index) % screenReadbackSlots.size();
187 readbackWriteSlot = slot;
188 const vk::Image
image = images[imageIndex];
193 vk::ImageMemoryBarrier toTransfer{};
194 toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
195 toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
196 toTransfer.oldLayout = vk::ImageLayout::ePresentSrcKHR;
197 toTransfer.newLayout = vk::ImageLayout::eTransferSrcOptimal;
198 toTransfer.image =
image;
199 toTransfer.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
203 toTransfer.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
204 toTransfer.dstAccessMask = vk::AccessFlagBits::eTransferRead;
205 cb.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput,
206 vk::PipelineStageFlagBits::eTransfer, {}, 0,
nullptr, 0,
nullptr, 1,
209 vk::BufferImageCopy region{};
210 region.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
212 cb.copyImageToBuffer(image, vk::ImageLayout::eTransferSrcOptimal,
213 screenReadbackSlots[slot].staging.buffer, region);
215 vk::ImageMemoryBarrier toPresent = toTransfer;
216 toPresent.oldLayout = vk::ImageLayout::eTransferSrcOptimal;
217 toPresent.newLayout = vk::ImageLayout::ePresentSrcKHR;
218 toPresent.srcAccessMask = vk::AccessFlagBits::eTransferRead;
219 toPresent.dstAccessMask = vk::AccessFlagBits::eMemoryRead;
220 cb.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
221 vk::PipelineStageFlagBits::eBottomOfPipe, {}, 0,
nullptr, 0,
nullptr, 1,
226void Graphics::syncReadbackCpu() {
227 if (!readbackReady || screenReadbackSlots.empty())
return;
228 if (readbackCpuSynced && !lastFrameRgba.empty())
return;
229 if (readbackWriteSlot >= screenReadbackSlots.size())
return;
231 const size_t bytes = screenReadbackBytes;
232 if (
bytes == 0)
return;
236 presentModel.waitForFrameSlot(readbackWriteSlot);
238 auto &slot = screenReadbackSlots[readbackWriteSlot];
240 slot.mapped = slot.staging.map();
242 lastFrameRgba.resize(
bytes);
245 const size_t words =
bytes / 8;
246 const uint64_t *src64 =
static_cast<const uint64_t *
>(slot.mapped);
247 uint64_t *dst64 =
reinterpret_cast<uint64_t *
>(lastFrameRgba.data());
248 for (
size_t i = 0; i < words; ++i) {
249 const uint64_t
v = src64[i];
250 dst64[i] = (
v & 0xFF00FF00FF00FF00ull) |
251 ((
v & 0x000000FF000000FFull) << 16) |
252 ((
v >> 16) & 0x000000FF000000FFull);
254 for (
size_t i = words * 8; i <
bytes; i += 4) {
255 const uint32_t
v = *
reinterpret_cast<const uint32_t *
>(
256 static_cast<const uint8_t *
>(slot.mapped) + i);
257 const uint32_t out = (
v & 0xFF00FF00u) | ((
v & 0xFFu) << 16) | ((
v >> 16) & 0xFFu);
258 std::memcpy(lastFrameRgba.data() + i, &out, 4);
261 std::memcpy(lastFrameRgba.data(), slot.mapped,
bytes);
263 readbackCpuSynced =
true;
266void Graphics::destroyReadbackResources() {
267 for (
auto &slot : screenReadbackSlots) {
269 slot.staging.unmap();
270 slot.mapped =
nullptr;
272 slot.staging.release();
274 screenReadbackSlots.clear();
275 screenReadbackBytes = 0;
276 readbackReady =
false;
277 readbackCpuSynced =
false;
278 hasPresentedFrame =
false;
283 if (!hasPresentedFrame || lastFrameRgba.empty())
284 throw Exception(
"Graphics::newImageData: no presented frame");
286 std::memcpy(img->getData(), lastFrameRgba.data(), lastFrameRgba.size());
292 if (!hasPresentedFrame || lastFrameRgba.empty())
293 throw Exception(
"Graphics::getPixel: no presented frame");
295 throw Exception(
"Graphics::getPixel: out of bounds (%d,%d)",
x,
y);
299 const int cx = std::min(std::max(pxX, 0),
pixelWidth - 1);
301 const size_t i = (size_t(
cy) * size_t(
pixelWidth) + size_t(
cx)) * 4;
302 float r = lastFrameRgba[i + 0] / 255.f;
303 float g = lastFrameRgba[i + 1] / 255.f;
304 float b = lastFrameRgba[i + 2] / 255.f;
305 float a = lastFrameRgba[i + 3] / 255.f;
307 const vk::Format fmt = swapchain.image_format;
308 if (fmt == vk::Format::eB8G8R8A8Srgb || fmt == vk::Format::eR8G8B8A8Srgb) {
309 auto toLinear = [](
float u) {
310 return (
u <= 0.04045f) ? (
u / 12.92f) : std::pow((
u + 0.055f) / 1.055f, 2.4f);
320 const std::vector<eve::graphics::Graphics::EntityIdDraw> &draws,
const glm::mat4 &
viewProj,
321 int maskW,
int maskH) {
322 if (!initialized || maskW <= 0 || maskH <= 0)
return nullptr;
325 createGBufferResources(maskW, maskH);
326 if (!gbufferPipeline || !gbufferRenderPass)
return nullptr;
327 auto *slot = currentGBufferSlot();
328 if (!slot || !slot->framebuffer || !whiteTexture)
return nullptr;
334 std::vector<GBufferDraw> idDraws;
335 idDraws.reserve(draws.size());
336 auto u8 = [](
float x) -> uint32_t {
337 return uint32_t(std::lround(std::clamp(
x, 0.f, 1.f) * 255.f));
339 for (
const auto &
d : draws) {
340 if (!
d.mesh || !
d.mesh->gpuHandle)
continue;
343 gd.albedo = whiteTexture;
345 gd.push.modelR0 = glm::vec4(
d.model[0][0],
d.model[1][0],
d.model[2][0],
d.model[3][0]);
346 gd.push.modelR1 = glm::vec4(
d.model[0][1],
d.model[1][1],
d.model[2][1],
d.model[3][1]);
347 gd.push.modelR2 = glm::vec4(
d.model[0][2],
d.model[1][2],
d.model[2][2],
d.model[3][2]);
348 const uint32_t packed = u8(
d.idColor.r) | (u8(
d.idColor.g) << 8) |
349 (u8(
d.idColor.b) << 16) | (u8(
d.idColor.a) << 24);
350 gd.push.clip = glm::vec4(0.1f, 100.f, glm::uintBitsToFloat(packed), 0.f);
351 idDraws.push_back(gd);
353 if (idDraws.empty())
return nullptr;
355 const uint32_t
w = uint32_t(maskW);
356 const uint32_t
h = uint32_t(maskH);
357 const vk::DeviceSize byteSize = vk::DeviceSize(
w) * vk::DeviceSize(
h) * 4;
358 vkb::GenericBuffer staging(device, vk::BufferUsageFlagBits::eTransferDst, byteSize,
359 vk::MemoryPropertyFlagBits::eHostVisible |
360 vk::MemoryPropertyFlagBits::eHostCoherent);
362 vkb::executeImmediately(device.instance, uploadPool, device.getQueue(vkb::QueueType::graphics),
363 [&](vk::CommandBuffer cb) {
364 std::array<vk::ClearValue, 4> clears{};
365 clears[0].color = vk::ClearColorValue(std::array<float, 4>{0, 0, 0, 0});
366 clears[1].color = vk::ClearColorValue(std::array<float, 4>{0, 0, 0, 0});
367 clears[2].color = vk::ClearColorValue(std::array<float, 4>{0, 0, 0, 0});
368 clears[3].depthStencil = vk::ClearDepthStencilValue{1.0f, 0};
369 vk::RenderPassBeginInfo rpBegin{};
370 rpBegin.renderPass = gbufferRenderPass;
371 rpBegin.framebuffer = slot->framebuffer;
372 rpBegin.renderArea = vk::Rect2D{{0, 0}, {
w,
h}};
373 rpBegin.clearValueCount = uint32_t(clears.size());
374 rpBegin.pClearValues = clears.data();
375 slot->normal.beginColorAttachment();
376 slot->depthColor.beginColorAttachment();
377 slot->albedo.beginColorAttachment();
378 slot->depth.beginDepthAttachment();
379 cb.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
380 setViewportAndScissor(cb,
w,
h);
381 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, gbufferPipeline);
382 for (
const auto &
d : idDraws) {
383 auto *gpuMesh =
static_cast<GpuMesh *
>(
d.mesh->gpuHandle);
384 if (!gpuMesh)
continue;
385 if (whiteTexture && whiteTexture->
gpuHandle && texSetLayout) {
387 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
388 gbufferPipelineLayout, 0, 1,
389 gpuTex->descriptorSet.ptr(), 0,
nullptr);
391 cb.pushConstants(gbufferPipelineLayout,
392 vk::ShaderStageFlagBits::eVertex |
393 vk::ShaderStageFlagBits::eFragment,
394 0,
sizeof(GBufferPush), &
d.push);
395 drawIndexedMesh(cb, *gpuMesh);
398 slot->normal.endSampledLayout();
399 slot->depthColor.endSampledLayout();
400 slot->albedo.endSampledLayout();
401 slot->depth.endSampledLayout();
404 slot->albedo.setLayout(cb, vk::ImageLayout::eTransferSrcOptimal);
405 vk::BufferImageCopy region{};
406 region.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
407 region.imageExtent = vk::Extent3D{
w,
h, 1};
408 cb.copyImageToBuffer(slot->albedo.image(),
409 vk::ImageLayout::eTransferSrcOptimal, staging.buffer,
411 slot->albedo.setLayout(cb, vk::ImageLayout::eShaderReadOnlyOptimal);
415 void *mapped = staging.map();
416 std::memcpy(img->getData(), mapped,
size_t(byteSize));
424 rc->getGBuffer()->setTargets(
int(
w),
int(
h), &slot->depthColorTex, &slot->normalTex, &slot->albedoTex,
425 &slot->depthTex, &slot->pbrParamsTex, &slot->emissiveTex);
431 if (!initialized)
return nullptr;
432 auto *slot = currentGBufferSlot();
433 if (!slot)
return nullptr;
434 vkb::ColorTarget *src =
nullptr;
435 if (attachment ==
"depth")
436 src = &slot->depthColor;
437 else if (attachment ==
"normal")
439 else if (attachment ==
"albedo")
441 else if (attachment ==
"pbrParams")
442 src = &slot->pbrParams;
443 else if (attachment ==
"emissive")
444 src = &slot->emissive;
448 const uint32_t
w = uint32_t(gbufferWidth);
449 const uint32_t
h = uint32_t(gbufferHeight);
450 if (
w == 0 ||
h == 0)
return nullptr;
452 const vk::DeviceSize byteSize = vk::DeviceSize(
w) * vk::DeviceSize(
h) * 4;
453 if (gbufferPending) recordDeferredFrameGraph();
454 vkb::GenericBuffer staging(
device, vk::BufferUsageFlagBits::eTransferDst, byteSize,
455 vk::MemoryPropertyFlagBits::eHostVisible |
456 vk::MemoryPropertyFlagBits::eHostCoherent);
457 vkb::executeImmediately(
device.instance, uploadPool,
device.getQueue(vkb::QueueType::graphics),
458 [&](vk::CommandBuffer cb) {
459 transitionSampledColorForTransfer(
460 cb, *src, vk::ImageLayout::eTransferSrcOptimal);
461 vk::BufferImageCopy region{};
462 region.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
463 region.imageExtent = vk::Extent3D{
w,
h, 1};
464 cb.copyImageToBuffer(src->image(), vk::ImageLayout::eTransferSrcOptimal,
465 staging.buffer, region);
466 transitionSampledColorForTransfer(
467 cb, *src, vk::ImageLayout::eShaderReadOnlyOptimal);
471 void *mapped = staging.map();
472 std::memcpy(img->getData(), mapped,
size_t(byteSize));
475 if (attachment ==
"depth") {
476 auto *
pixels =
static_cast<uint8_t *
>(img->getData());
477 const size_t pixelCount = size_t(
w) * size_t(
h);
478 for (
size_t i = 0; i < pixelCount; ++i) {
487 if (!initialized)
return nullptr;
488 auto *slot = currentDecalSlot();
489 auto *gslot = currentGBufferSlot();
490 if (!slot || !gslot || !slot->framebuffer || !decalPipeline || !decalRenderPass || !gbufferPipeline ||
491 !gbufferRenderPass || !gslot->framebuffer)
493 vkb::ColorTarget *src =
nullptr;
494 if (attachment ==
"normal")
496 else if (attachment ==
"params")
498 else if (attachment ==
"albedo")
503 const uint32_t
w = uint32_t(decalWidth);
504 const uint32_t
h = uint32_t(decalHeight);
505 if (
w == 0 ||
h == 0)
return nullptr;
506 if (gbufferPending) recordDeferredFrameGraph();
508 const vk::DeviceSize byteSize = vk::DeviceSize(
w) * vk::DeviceSize(
h) * 4;
509 vkb::GenericBuffer staging(
device, vk::BufferUsageFlagBits::eTransferDst, byteSize,
510 vk::MemoryPropertyFlagBits::eHostVisible |
511 vk::MemoryPropertyFlagBits::eHostCoherent);
512 vkb::executeImmediately(
device.instance, uploadPool,
device.getQueue(vkb::QueueType::graphics),
513 [&](vk::CommandBuffer cb) {
516 recordDecalPassInto(cb, *slot, *gslot);
517 transitionSampledColorForTransfer(
518 cb, *src, vk::ImageLayout::eTransferSrcOptimal);
519 vk::BufferImageCopy region{};
520 region.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0,
522 region.imageExtent = vk::Extent3D{
w,
h, 1};
523 cb.copyImageToBuffer(src->image(),
524 vk::ImageLayout::eTransferSrcOptimal,
525 staging.buffer, region);
526 transitionSampledColorForTransfer(
527 cb, *src, vk::ImageLayout::eShaderReadOnlyOptimal);
531 void *mapped = staging.map();
532 std::memcpy(img->getData(), mapped,
size_t(byteSize));
542 if (!initialized)
throw Exception(
"newCanvas: graphics not initialized");
543 if (
w <= 0 ||
h <= 0)
throw Exception(
"newCanvas: invalid size");
544 ensureOffscreenPipelines();
545 auto c = std::make_unique<OffscreenCanvas>(
this,
w,
h);
547 ownedCanvases.push_back(std::move(
c));
555 if (!initialized)
throw Exception(
"newHDRCanvas: graphics not initialized");
556 if (
w <= 0 ||
h <= 0)
throw Exception(
"newHDRCanvas: invalid size");
557 ensureHdrOffscreenPipelines();
558 auto c = std::make_unique<OffscreenCanvas>(
this,
w,
h,
true);
560 ownedCanvases.push_back(std::move(
c));
566 if (next ==
static_cast<Canvas *
>(
this)) next =
nullptr;
567 if (next == activeCanvas)
return;
568 bool hasSolid =
false;
569 for (
const auto &sb : solidBatches)
570 if (!sb.batch.empty()) hasSolid =
true;
571 if (hasSolid || !texturedBatches.empty() || !litBatches.empty()) flushBatch();
572 if (next && sceneColorPassOpen) {
573 endSceneColorRenderPass();
574 queueSceneColorResolve();
580 return activeCanvas !=
nullptr;
584 return activeCanvas ? activeCanvas :
const_cast<Graphics *
>(
this);
593 if (initialized && changed) swapchainDirty =
true;
596void Graphics::clear2DBatches() {
597 solidBatches.
clear();
598 texturedBatches.clear();
600 overlaySpans.clear();
601 engine3DSpans.clear();
602 gpuParticleDraws_.clear();
603 pendingSceneResolve.reset();
604 pendingUiResolve.reset();
605 sceneColorComposited =
false;
608void Graphics::noteSolidOverlay(uint32_t
idx) {
609 if (
idx >= solidBatches.size())
return;
610 const uint32_t
n = uint32_t(solidBatches[
idx].batch.vertices().size());
612 if (!spans.empty() && spans.back().kind == OverlayKind::Solid &&
613 spans.back().index ==
idx) {
614 spans.back().vertCount =
n - spans.back().vertBegin;
617 const uint32_t
begin =
n >= 6u ?
n - 6u : 0
u;
621void Graphics::noteTexturedOverlay(Texture *tex, uint32_t
idx) {
626 sceneColorComposited =
true;
628 if (!spans.empty() && spans.back().kind == OverlayKind::Textured && spans.back().index ==
idx)
630 spans.push_back({OverlayKind::Textured,
idx, 0, 0});
633void Graphics::noteLitOverlay(uint32_t
idx) {
635 if (!spans.empty() && spans.back().kind == OverlayKind::Lit && spans.back().index ==
idx)
637 spans.push_back({OverlayKind::Lit,
idx, 0, 0});
642 if (
frameHad3D && activeCanvas ==
nullptr)
return;
644 hasPendingClear =
true;
647 oc->clear(
clearColor, std::nullopt, std::nullopt);
653 auto it = std::find_if(solidBatches.begin(), solidBatches.end(),
654 [&](
const SolidBatch &sb) { return sb.blend == blend; });
655 if (it == solidBatches.end()) {
657 it = solidBatches.end() - 1;
660 noteSolidOverlay(uint32_t(it - solidBatches.begin()));
664 const int targetWidth = activeCanvas ? activeCanvas->getWidth() :
getWidth();
665 const int targetHeight = activeCanvas ? activeCanvas->getHeight() :
getHeight();
669 const glm::vec2 ndc = glm::vec2(vertex.clipPosition) / vertex.clipPosition.w;
670 return glm::vec2((ndc.x + 1.f) * 0.5f *
static_cast<float>(targetWidth),
671 (ndc.y + 1.f) * 0.5f *
static_cast<float>(targetHeight));
675 auto it = std::find_if(solidBatches.begin(), solidBatches.end(),
676 [&](
const SolidBatch &batch) { return batch.blend == resolvedBatch.blend; });
677 if (it == solidBatches.end()) {
678 solidBatches.push_back(SolidBatch{resolvedBatch.blend,
Batcher{}});
679 it = solidBatches.end() - 1;
681 for (std::size_t i = resolvedBatch.firstVertex; i < resolvedBatch.firstVertex + resolvedBatch.vertexCount;
683 it->batch.addTriangle(logicalPoint(
triangles.vertices[i]), logicalPoint(
triangles.vertices[i + 1]),
687 const auto batchIndex =
static_cast<std::uint32_t
>(it - solidBatches.begin());
688 const std::uint32_t
count =
static_cast<std::uint32_t
>(resolvedBatch.vertexCount);
689 const std::uint32_t
end =
static_cast<std::uint32_t
>(it->batch.vertices().size());
690 spans.push_back({OverlayKind::Solid, batchIndex,
end -
count,
count});
696 auto it = std::find_if(solidBatches.begin(), solidBatches.end(),
697 [&](
const SolidBatch &sb) { return sb.blend == blend; });
698 if (it == solidBatches.end()) {
700 it = solidBatches.end() - 1;
703 noteSolidOverlay(uint32_t(it - solidBatches.begin()));
710 auto wrapMode = [](
bool repeat) {
711 return repeat ? vk::SamplerAddressMode::eRepeat : vk::SamplerAddressMode::eClampToEdge;
718 : vk::SamplerMipmapMode::eLinear;
722 float maxLod = useMips ? std::min(
sampler.maxLod,
float(mipLevels - 1)) : 0.f;
726 bool enableAniso =
false;
727 if (
sampler.maxAnisotropy > 1.f && maxSamplerAnisotropy > 1.f) {
729 aniso = std::min(
sampler.maxAnisotropy, maxSamplerAnisotropy);
732 vkb::SamplerBuilder sb;
733 return sb.magFilter(toFilter(
sampler.mag))
734 .minFilter(toFilter(
sampler.min))
735 .mipmapMode(useMips ? toMip(
sampler.mipmap) : vk::SamplerMipmapMode::eNearest)
738 .addressModeW(wrapMode(
sampler.repeatW))
740 .anisotropyEnable(enableAniso ? VK_TRUE : VK_FALSE)
741 .maxAnisotropy(aniso)
742 .minLod(useMips ?
sampler.minLod : 0.f)
754 drawTexturedRectShaderUV(texture,
shader,
x,
y,
w,
h, 0.f, 0.f, 1.f, 1.f,
color);
758 float v0,
float u1,
float v1,
const Color &
color) {
759 drawTexturedRectShaderUV(texture,
currentShader,
x,
y,
w,
h, u0, v0, u1, v1,
color);
763 float h,
float u0,
float v0,
float u1,
float v1,
769 if (texturedBatches.empty() || texturedBatches.back().texture != texture ||
770 texturedBatches.back().
depth !=
nullptr ||
771 texturedBatches.back().shader !=
shader ||
772 texturedBatches.back().blend !=
blend) {
775 texturedBatches.back().batch.addTexturedRect(
x,
y,
w,
h,
color, u0, v0, u1, v1, rotatedUV);
776 noteTexturedOverlay(texture, uint32_t(texturedBatches.size() - 1));
780 float w,
float h,
float degrees,
float u0,
float v0,
787 if (texturedBatches.empty() || texturedBatches.back().texture != texture ||
788 texturedBatches.back().
depth !=
nullptr ||
789 texturedBatches.back().shader !=
shader ||
790 texturedBatches.back().blend !=
blend) {
793 texturedBatches.back().batch.addTexturedRectRotated(
cx,
cy,
w,
h,
degrees,
color, u0, v0, u1, v1,
795 noteTexturedOverlay(texture, uint32_t(texturedBatches.size() - 1));
808 if (texturedBatches.empty() || texturedBatches.back().texture !=
color ||
809 texturedBatches.back().depth !=
depth || texturedBatches.back().shader !=
shader) {
810 texturedBatches.push_back(
813 texturedBatches.back().batch.addTexturedRect(
x,
y,
w,
h,
tint, 0.f, 0.f, 1.f, 1.f);
814 noteTexturedOverlay(
color, uint32_t(texturedBatches.size() - 1));
824 if (texturedBatches.empty() || texturedBatches.back().texture !=
color ||
825 texturedBatches.back().depth !=
depth || texturedBatches.back().motion != motion ||
826 texturedBatches.back().shader !=
shader) {
828 batch.motion = motion;
829 texturedBatches.push_back(std::move(batch));
831 texturedBatches.back().batch.addTexturedRect(
x,
y,
w,
h,
tint, 0.f, 0.f, 1.f, 1.f);
832 noteTexturedOverlay(
color, uint32_t(texturedBatches.size() - 1));
842 if (texturedBatches.empty() || texturedBatches.back().texture !=
color ||
843 texturedBatches.back().depth !=
depth || texturedBatches.back().motion != motion ||
844 texturedBatches.back().extra != extra || texturedBatches.back().shader !=
shader) {
846 batch.motion = motion;
848 texturedBatches.push_back(std::move(batch));
850 texturedBatches.back().batch.addTexturedRect(
x,
y,
w,
h,
tint, 0.f, 0.f, 1.f, 1.f);
851 noteTexturedOverlay(
color, uint32_t(texturedBatches.size() - 1));
861 if (texturedBatches.empty() || texturedBatches.back().texture !=
color ||
862 texturedBatches.back().depth !=
depth || texturedBatches.back().motion != motion ||
863 texturedBatches.back().extra != extra || texturedBatches.back().specular != specular ||
864 texturedBatches.back().shader !=
shader) {
866 batch.motion = motion;
868 batch.specular = specular;
869 texturedBatches.push_back(std::move(batch));
871 texturedBatches.back().batch.addTexturedRect(
x,
y,
w,
h,
tint, 0.f, 0.f, 1.f, 1.f);
872 noteTexturedOverlay(
color, uint32_t(texturedBatches.size() - 1));
877 float u1,
float v1,
float strengthPixels,
float opacity,
bool rotatedUV) {
879 if (!displacement || !
scene || !
scene->gpuHandle || !particleDistortionPipeline)
883 batch.effect = TexturedBatch::Effect::SceneColorDistortion;
885 Color(strengthPixels, 0.f, 0.f,
opacity), u0, v0, u1, v1,
887 texturedBatches.push_back(std::move(batch));
890 noteTexturedOverlay(
nullptr, uint32_t(texturedBatches.size() - 1));
894void Graphics::drawUiTextureRects(
void* commandBuffer,
const std::vector<UiTextureDraw>& draws,
895 std::size_t bufferOffset) {
896 if (!commandBuffer || draws.empty() || !uiTexturePipeline || uiColorWidth <= 0 ||
900 vk::CommandBuffer cb(
static_cast<VkCommandBuffer
>(commandBuffer));
901 auto &buffers = currentFrame2DBuffers().uiTexBufs;
902 std::size_t bufferIndex = bufferOffset;
903 setViewportAndScissor(cb, uint32_t(uiColorWidth), uint32_t(uiColorHeight));
906 if (!
draw.texture || !
draw.texture->gpuHandle ||
draw.w <= 0.f ||
draw.h <= 0.f)
915 batch.
toNDC(uiColorWidth, uiColorHeight);
916 std::vector<TexturedVertex>
vertices;
918 for (
const auto &vertex : batch.
vertices())
921 while (bufferIndex >= buffers.size()) buffers.emplace_back();
922 vkb::HostVertexBuffer &vertexBuffer = buffers[bufferIndex++];
925 const int clipX = std::clamp(
int(std::floor(
draw.clipX)), 0, uiColorWidth);
926 const int clipY = std::clamp(
int(std::floor(
draw.clipY)), 0, uiColorHeight);
927 const int clipRight = std::clamp(
int(std::ceil(
draw.clipX +
draw.clipW)), 0, uiColorWidth);
928 const int clipBottom = std::clamp(
int(std::ceil(
draw.clipY +
draw.clipH)), 0, uiColorHeight);
929 if (clipRight <= clipX || clipBottom <= clipY)
continue;
930 const vk::Rect2D scissor{{clipX, clipY},
931 {uint32_t(clipRight - clipX), uint32_t(clipBottom - clipY)}};
932 cb.setScissor(0, 1, &scissor);
933 cb.bindPipeline(vk::PipelineBindPoint::eGraphics,
934 draw.opaque ? uiTextureOpaquePipeline : uiTexturePipeline);
935 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, texPipelineLayout, 0, 1, &set, 0,
937 const vk::DeviceSize
offset = 0;
938 cb.bindVertexBuffers(0, 1, vertexBuffer, &
offset);
939 cb.draw(uint32_t(
vertices.size()), 1, 0, 0);
945void Graphics::ensureFlatNormalTexture() {
946 if (flatNormalTexture)
return;
947 const uint8_t
px[4] = {128, 128, 255, 255};
957 ensureFlatNormalTexture();
959 if (litBatches.empty() || litBatches.back().albedo !=
albedo || litBatches.back().normal !=
normal ||
960 litBatches.back().blend !=
blend) {
963 litBatches.back().batch.addTexturedRect(
x,
y,
w,
h,
color, u0, v0, u1, v1);
964 noteLitOverlay(uint32_t(litBatches.size() - 1));
974 ensureFlatNormalTexture();
976 if (litBatches.empty() || litBatches.back().albedo !=
albedo || litBatches.back().normal !=
normal ||
977 litBatches.back().blend !=
blend) {
980 litBatches.back().batch.addTexturedRectRotated(
cx,
cy,
w,
h,
degrees,
color, u0, v0, u1, v1);
981 noteLitOverlay(uint32_t(litBatches.size() - 1));
985 ASSERT(
albedo !=
nullptr);
986 ASSERT(
normal !=
nullptr);
987 auto &sets = offscreen ? offscreenLit2dSets : currentLit2dSets();
988 vkb::GenericBuffer &ubo = offscreen ? offscreenLighting2dUbo : currentLighting2dUbo();
990 auto it = sets.find(
key);
991 if (it != sets.end())
return it->second;
993 vk::DescriptorSetAllocateInfo alloc{};
994 alloc.descriptorPool = descriptorPool;
995 alloc.descriptorSetCount = 1;
996 alloc.pSetLayouts = &lit2dSetLayout;
997 vkb::UnboundSet unbound{
device->allocateDescriptorSets(alloc).front()};
999 vkb::DescriptorSetUpdater updater;
1000 updater.beginDescriptorSet(unbound)
1001 .beginImages(0, 0, vk::DescriptorType::eCombinedImageSampler)
1002 .image(vkb::SampledImage::forLaterSample(
albedo->sampler,
albedo->image.imageView()))
1003 .beginImages(1, 0, vk::DescriptorType::eCombinedImageSampler)
1004 .image(vkb::SampledImage::forLaterSample(
normal->sampler,
normal->image.imageView()))
1005 .beginBuffers(2, 0, vk::DescriptorType::eUniformBuffer)
1006 .buffer(ubo.buffer, 0,
sizeof(Lighting2DUBO))
1007 .update(
device.instance);
1009 vkb::BoundSet bound = std::move(unbound).publish();
1010 sets.emplace(
key, bound);
1014vkb::BoundSet Graphics::post2SetFor(GpuTexture *
color, GpuTexture *
depth, GpuTexture *motion,
1015 GpuTexture *extra, GpuTexture *specular) {
1017 vk::ImageView colorView =
color->imageView();
1018 if (!colorView)
return {};
1020 if (!motion || !motion->sampler || !motion->imageView()) motion =
color;
1021 if (!extra || !extra->sampler || !extra->imageView()) extra =
color;
1022 if (!specular || !specular->sampler || !specular->imageView()) specular =
color;
1024 auto it = post2Sets.find(
key);
1025 if (it != post2Sets.end())
return it->second;
1027 auto sets = vkb::DescriptorSetBuilder().layout(texSetLayout).build(
device.instance, descriptorPool);
1029 vkb::UnboundSet unbound{sets[0]};
1030 vkb::DescriptorSetUpdater updater;
1031 updater.beginDescriptorSet(unbound)
1032 .beginImages(0, 0, vk::DescriptorType::eCombinedImageSampler)
1033 .image(vkb::SampledImage::forLaterSample(
color->sampler, colorView))
1034 .beginImages(1, 0, vk::DescriptorType::eCombinedImageSampler)
1035 .image(vkb::SampledImage::forLaterSample(
depth->sampler,
depth->imageView()))
1036 .beginImages(2, 0, vk::DescriptorType::eCombinedImageSampler)
1037 .image(vkb::SampledImage::forLaterSample(motion->sampler, motion->imageView()))
1038 .beginImages(3, 0, vk::DescriptorType::eCombinedImageSampler)
1039 .image(vkb::SampledImage::forLaterSample(extra->sampler, extra->imageView()))
1040 .beginImages(4, 0, vk::DescriptorType::eCombinedImageSampler)
1041 .image(vkb::SampledImage::forLaterSample(specular->sampler, specular->imageView()))
1042 .update(
device.instance);
1043 vkb::BoundSet bound = std::move(unbound).publish();
1044 post2Sets.emplace(
key, bound);
1048void Graphics::drawLitBatches(vk::CommandBuffer cb,
int viewW,
int viewH, std::vector<LitBatch> &batches,
1049 std::vector<vkb::HostVertexBuffer> &texBufs,
size_t &texBufIndex,
bool offscreen,
1051 if (batches.empty() || !lit2dPipelineLayout)
return;
1052 lighting2dFrame.meta.y = float(viewW);
1053 lighting2dFrame.meta.z = float(viewH);
1054 vkb::GenericBuffer &ubo = offscreen ? offscreenLighting2dUbo : currentLighting2dUbo();
1055 ubo.updateLocal(frameToken(), &lighting2dFrame,
sizeof(Lighting2DUBO));
1057 for (
auto &lb : batches) {
1058 if (lb.batch.empty() || !lb.albedo || !lb.albedo->gpuHandle)
continue;
1059 vk::Pipeline litPipeline = selectLit2DPipeline(lb.blend, offscreen, hdr);
1060 if (!litPipeline)
continue;
1061 ensureFlatNormalTexture();
1062 Texture *ntex = lb.normal ? lb.normal : flatNormalTexture;
1063 if (!ntex || !ntex->gpuHandle)
continue;
1064 auto *albedoGpu =
static_cast<GpuTexture *
>(lb.albedo->gpuHandle);
1065 auto *normalGpu =
static_cast<GpuTexture *
>(ntex->gpuHandle);
1067 Batcher ndc = lb.batch;
1068 ndc.toNDC(viewW, viewH);
1069 std::vector<TexturedVertex> gpuVerts;
1070 gpuVerts.reserve(ndc.vertices().size());
1072 gpuVerts.push_back(TexturedVertex{
v.pos,
v.color,
v.uv});
1075 if (texBufIndex >= texBufs.size()) texBufs.emplace_back();
1076 vkb::HostVertexBuffer &vb = texBufs[texBufIndex++];
1077 vb.allocate<TexturedVertex>(frameToken(),
device, gpuVerts);
1079 vk::DescriptorSet set = lit2dSetFor(albedoGpu, normalGpu, offscreen);
1080 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, litPipeline);
1081 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, lit2dPipelineLayout, 0, 1, &set, 0,
1083 vk::DeviceSize
offset = 0;
1084 cb.bindVertexBuffers(0, 1, vb, &
offset);
1085 cb.draw(uint32_t(gpuVerts.size()), 1, 0, 0);
1091 const std::vector<uint32_t> &fragSpv) {
1092 ASSERT(initialized);
1093 if (!initialized)
throw Exception(
"newShaderFromSpv: graphics not initialized");
1094 if (fragSpv.empty())
throw Exception(
"newShaderFromSpv: empty fragment SPIR-V");
1096 std::vector<uint32_t>
vert = vertSpv;
1098 vert.assign(textured_vert_spv, textured_vert_spv + textured_vert_spv_count);
1099 if (
vert[0] != 0x07230203 || fragSpv[0] != 0x07230203)
1100 throw Exception(
"newShaderFromSpv: SPIR-V magic mismatch");
1102 auto gpu = std::make_unique<GpuShader>();
1103 gpu->pipelineLayout = shaderPipelineLayout;
1104 gpu->swapchainPipeline =
1105 createTexturedStylePipeline(
vert, fragSpv, renderpass, shaderPipelineLayout);
1106 gpu->swapchainOpaquePipeline = createTexturedStylePipeline(
1108 if (offscreenRenderPass) {
1109 gpu->offscreenPipeline =
1110 createTexturedStylePipeline(
vert, fragSpv, offscreenRenderPass, shaderPipelineLayout);
1111 gpu->offscreenOpaquePipeline = createTexturedStylePipeline(
1115 auto sh = std::make_unique<Shader>();
1116 sh->setSpirv(std::move(
vert), fragSpv);
1117 sh->gpuHandle = gpu.get();
1120 ownedShaders.push_back(std::move(sh));
1121 ownedGpuShaders.push_back(std::move(gpu));
1126 if (fragPath.empty())
throw Exception(
"newShaderFromSpvFile: empty fragPath");
1127 std::vector<uint32_t>
vert;
1128 if (!vertPath.empty())
vert = readSpirvFile(vertPath);
1129 auto frag = readSpirvFile(fragPath);
1134 if (fragGlsl.empty())
throw Exception(
"newShader: empty fragment GLSL");
1135 std::vector<uint32_t>
vert;
1142 const std::vector<uint32_t> &fragSpv) {
1143 ASSERT(initialized);
1144 if (!initialized)
throw Exception(
"newMeshShaderFromSpv: graphics not initialized");
1145 createMesh3DPipeline();
1146 if (!mesh3dShaderPipelineLayout)
1147 throw Exception(
"newMeshShaderFromSpv: mesh3d pipeline layout missing");
1149 std::vector<uint32_t>
vert = vertSpv;
1150 std::vector<uint32_t>
frag = fragSpv;
1152 vert.assign(mesh3d_vert_spv, mesh3d_vert_spv + mesh3d_vert_spv_count);
1154 frag.assign(mesh3d_frag_spv, mesh3d_frag_spv + mesh3d_frag_spv_count);
1155 if (
vert[0] != 0x07230203 ||
frag[0] != 0x07230203)
1156 throw Exception(
"newMeshShaderFromSpv: SPIR-V magic mismatch");
1158 auto gpu = std::make_unique<GpuShader>();
1159 gpu->isMesh3D =
true;
1160 gpu->pipelineLayout = mesh3dShaderPipelineLayout;
1161 ensureOffscreen3DResources();
1162 gpu->mesh3dPipeline = createMesh3DStylePipeline(
vert,
frag, mesh3dShaderPipelineLayout,
1163 activeScenePass(), activeSceneSamples());
1166 gpu->mesh3dXrayPipeline = createMesh3DXrayPipeline(
vert,
frag, mesh3dShaderPipelineLayout,
1167 activeScenePass(), activeSceneSamples());
1168 gpu->mesh3dOffscreenPipeline = createMesh3DStylePipeline(
1169 vert,
frag, mesh3dShaderPipelineLayout, offscreen3DRenderPass,
1170 vk::SampleCountFlagBits::e1);
1171 gpu->mesh3dHdrOffscreenPipeline = createMesh3DStylePipeline(
1172 vert,
frag, mesh3dShaderPipelineLayout, hdrOffscreen3DRenderPass,
1173 vk::SampleCountFlagBits::e1);
1175 auto sh = std::make_unique<Shader>();
1177 sh->setSpirv(std::move(
vert), std::move(
frag));
1178 sh->gpuHandle = gpu.get();
1179 gpu->owner = sh.get();
1182 ownedShaders.push_back(std::move(sh));
1183 ownedGpuShaders.push_back(std::move(gpu));
1188 const std::vector<uint32_t> &fragSpv) {
1189 ASSERT(initialized);
1190 if (!initialized)
throw Exception(
"newHairShaderFromSpv: graphics not initialized");
1191 createMesh3DPipeline();
1192 if (fragSpv.empty())
throw Exception(
"newHairShaderFromSpv: empty fragment SPIR-V");
1193 if (!mesh3dShaderPipelineLayout)
1194 throw Exception(
"newHairShaderFromSpv: mesh3d pipeline layout missing");
1196 std::vector<uint32_t>
vert = vertSpv;
1198 vert.assign(mesh3d_hair_vert_spv, mesh3d_hair_vert_spv + mesh3d_hair_vert_spv_count);
1199 if (
vert[0] != 0x07230203 || fragSpv[0] != 0x07230203)
1200 throw Exception(
"newHairShaderFromSpv: SPIR-V magic mismatch");
1202 auto gpu = std::make_unique<GpuShader>();
1203 gpu->isMesh3D =
true;
1204 gpu->isHair3D =
true;
1205 gpu->pipelineLayout = mesh3dShaderPipelineLayout;
1206 ensureOffscreen3DResources();
1207 gpu->mesh3dPipeline = createMesh3DHairPipeline(
vert, fragSpv, mesh3dShaderPipelineLayout,
1208 activeScenePass(), activeSceneSamples());
1209 gpu->mesh3dOffscreenPipeline = createMesh3DHairPipeline(
1210 vert, fragSpv, mesh3dShaderPipelineLayout, offscreen3DRenderPass,
1211 vk::SampleCountFlagBits::e1);
1212 gpu->mesh3dHdrOffscreenPipeline = createMesh3DHairPipeline(
1213 vert, fragSpv, mesh3dShaderPipelineLayout, hdrOffscreen3DRenderPass,
1214 vk::SampleCountFlagBits::e1);
1216 auto sh = std::make_unique<Shader>();
1218 sh->setSpirv(std::move(
vert), fragSpv);
1219 sh->gpuHandle = gpu.get();
1220 gpu->owner = sh.get();
1223 ownedShaders.push_back(std::move(sh));
1224 ownedGpuShaders.push_back(std::move(gpu));
1229 throw Exception(
"newHairShaderFromWgsl: WGSL is only supported on the WebGPU backend");
1236 auto gpuIt = std::find_if(ownedGpuShaders.begin(), ownedGpuShaders.end(),
1237 [&](
const std::unique_ptr<GpuShader> &
g) {
1238 return g.get() == gpu;
1240 if (gpuIt == ownedGpuShaders.end())
return false;
1242 auto shIt = std::find_if(ownedShaders.begin(), ownedShaders.end(),
1243 [&](
const std::unique_ptr<Shader> &
s) {
1244 return s.get() == shader;
1246 if (shIt == ownedShaders.end())
return false;
1250 waitForSharedGpuResources();
1251 if (gpu->swapchainPipeline)
device->destroyPipeline(gpu->swapchainPipeline);
1252 if (gpu->offscreenPipeline)
device->destroyPipeline(gpu->offscreenPipeline);
1253 if (gpu->swapchainOpaquePipeline)
device->destroyPipeline(gpu->swapchainOpaquePipeline);
1254 if (gpu->offscreenOpaquePipeline)
device->destroyPipeline(gpu->offscreenOpaquePipeline);
1255 if (gpu->hdrOffscreenPipeline)
device->destroyPipeline(gpu->hdrOffscreenPipeline);
1256 if (gpu->hdrOffscreenOpaquePipeline)
1257 device->destroyPipeline(gpu->hdrOffscreenOpaquePipeline);
1258 if (gpu->mesh3dPipeline)
device->destroyPipeline(gpu->mesh3dPipeline);
1259 if (gpu->mesh3dXrayPipeline)
device->destroyPipeline(gpu->mesh3dXrayPipeline);
1260 if (gpu->mesh3dOffscreenPipeline)
1261 device->destroyPipeline(gpu->mesh3dOffscreenPipeline);
1262 if (gpu->mesh3dHdrOffscreenPipeline)
1263 device->destroyPipeline(gpu->mesh3dHdrOffscreenPipeline);
1264 shader->gpuHandle =
nullptr;
1265 ownedGpuShaders.erase(gpuIt);
1267 (void)shIt->release();
1268 ownedShaders.erase(shIt);
1273 if (fragGlsl.empty())
throw Exception(
"newMeshShader: empty fragment GLSL");
1274 std::vector<uint32_t>
vert;
1281 throw Exception(
"newMeshShaderFromWgsl: WGSL mesh shaders are only supported on the "
1282 "WebGPU backend; use newMeshShaderFromSpv on Vulkan.");
1286 throw Exception(
"newShaderFromWgsl: WGSL shaders are only supported on the WebGPU "
1287 "backend; use newShaderFromSpv on Vulkan.");
1290void Graphics::flushBatch() {
1292 if (!initialized)
return;
1295 if (!oc)
throw Exception(
"flushBatch: active canvas is not an OffscreenCanvas");
1296 flushToOffscreen(oc);
1302void Graphics::abortOpen3DFrame() {
1303 const bool hadScene = sceneColorPassOpen;
1304 const bool had3D = swapchainPassOpen;
1306 if (hadScene) endSceneColorRenderPass();
1308 sceneColorPassOpen =
false;
1314 if (hadScene) beginSwapchainColorPass();
1315 presentRecording = swapchainPass.endRenderPass();
1318 ? recordSwapchainReadback(presentRecording.commandBuffer())
1320 presentRecording.end().submitAndPresent();
1321 presentRecording = {};
1323 hasPresentedFrame =
true;
1324 readbackReady =
true;
1325 readbackCpuSynced =
false;
1330 presentRecording = {};
1332 swapchainPassOpen =
false;
1333 sceneColorPassOpen =
false;
1335 hasPendingClear =
false;
1336 flushingSwapchain_ =
false;
1340void Graphics::flushToSwapchain() {
1341 if (flushingSwapchain_)
return;
1342 flushingSwapchain_ =
true;
1343 bool completed =
false;
1348 g->flushingSwapchain_ =
false;
1349 if (!*completed)
g->abortOpen3DFrame();
1351 } guard{
this, &completed};
1353 const bool continue3D = swapchainPassOpen;
1354 const bool hadScenePass = sceneColorPassOpen;
1355 const bool hasScenePath = hadScenePass || pendingSceneResolveSource !=
nullptr;
1358 endSceneColorRenderPass();
1359 queueSceneColorResolve();
1366 if (!beginPresentCommandBuffer()) {
1367 dropPendingOffscreenPasses();
1368 hasPendingClear =
false;
1372 recordDeferredFrameGraph();
1375 if (!(continue3D && !hasScenePath))
1376 recordGpuParticleCompute(presentRecording.commandBuffer());
1378 materializeSceneColorResolve();
1384 renderUiOverlayPass();
1390 const bool useHdrCompose =
1392 if (useHdrCompose) {
1393 if (!beginPresentComposePass()) {
1395 beginSwapchainColorPass();
1396 swapchainPassOpen =
true;
1398 }
else if (hasScenePath || !continue3D) {
1399 beginSwapchainColorPass();
1400 swapchainPassOpen =
true;
1403 auto solid = std::move(solidBatches);
1404 auto textured = std::move(texturedBatches);
1405 auto lit = std::move(litBatches);
1406 auto spans = std::move(overlaySpans);
1407 auto engineSpans = std::move(engine3DSpans);
1408 auto gpuParticleDraws = std::move(gpuParticleDraws_);
1409 auto sceneResolve = std::move(pendingSceneResolve);
1410 auto uiResolve = std::move(pendingUiResolve);
1411 const bool autoScene = sceneResolve.has_value() && !sceneColorComposited;
1415 auto &cb = currentPresentCb();
1416 setViewportAndScissor(cb, swapchain.extent.width, swapchain.extent.height);
1420 auto &frameBufs = currentFrame2DBuffers();
1421 std::vector<vkb::HostVertexBuffer> &solidBufs = frameBufs.solidBufs;
1422 std::vector<vkb::HostVertexBuffer> &texBufs = frameBufs.texBufs;
1423 size_t texBufIndex = 0;
1425 auto swapchainTexPipe = [&](
BlendMode mode) -> vk::Pipeline {
1426 if (presentComposeActive_) {
1429 return hdrOffscreenAdditiveTexPipeline;
1431 return hdrOffscreenPremultipliedTexPipeline;
1433 return hdrOffscreenMultiplyTexPipeline;
1435 return hdrOffscreenOpaqueTexPipeline;
1438 return hdrOffscreenTexPipeline;
1443 return additiveTexPipeline;
1445 return premultipliedTexPipeline;
1447 return multiplyTexPipeline;
1449 return opaqueTexPipeline;
1455 auto swapchainSolidPipe = [&](
BlendMode mode) -> vk::Pipeline {
1456 if (presentComposeActive_) {
1459 return hdrOffscreenAdditiveSolidPipeline;
1461 return hdrOffscreenPremultipliedSolidPipeline;
1463 return hdrOffscreenMultiplySolidPipeline;
1465 return hdrOffscreenSolidAlphaPipeline;
1468 return hdrOffscreenSolidPipeline;
1473 return additiveSolidPipeline;
1475 return premultipliedSolidPipeline;
1477 return multiplySolidPipeline;
1479 return solidAlphaPipeline;
1485 const vk::Pipeline tonemapPipe =
1486 presentComposeActive_ && hdrOffscreenTonemapPipeline ? hdrOffscreenTonemapPipeline
1487 : sceneTonemapPipeline;
1488 const vk::Pipeline distortionPipe =
1489 presentComposeActive_ && hdrOffscreenParticleDistortionPipeline
1490 ? hdrOffscreenParticleDistortionPipeline
1491 : particleDistortionPipeline;
1492 const bool litAvailable = lit2dPipeline || (presentComposeActive_ && hdrOffscreenLitPipeline);
1494 auto drawTextured = [&](TexturedBatch &
tb,
bool toneMapScene =
false) {
1495 if (
tb.batch.empty() || !
tb.texture || !
tb.texture->gpuHandle)
return;
1496 auto *gpu =
static_cast<GpuTexture *
>(
tb.texture->gpuHandle);
1497 vk::DescriptorSet texSet = gpu->descriptorSet;
1498 Texture *depthTexture = toneMapScene ? sceneTex :
tb.depth;
1499 if ((depthTexture && depthTexture->gpuHandle) || (
tb.motion &&
tb.motion->gpuHandle) ||
1500 (
tb.extra &&
tb.extra->gpuHandle)) {
1502 depthTexture ?
static_cast<GpuTexture *
>(depthTexture->gpuHandle) : nullptr;
1504 tb.motion ?
static_cast<GpuTexture *
>(
tb.motion->gpuHandle) : nullptr;
1505 auto *extraGpu =
tb.extra ?
static_cast<GpuTexture *
>(
tb.extra->gpuHandle) : nullptr;
1507 tb.specular ?
static_cast<GpuTexture *
>(
tb.specular->gpuHandle) : nullptr;
1508 if (vk::DescriptorSet combo =
1509 post2SetFor(gpu, depthGpu, motionGpu, extraGpu, specularGpu))
1512 Batcher ndc =
tb.batch;
1514 std::vector<TexturedVertex> gpuVerts;
1515 gpuVerts.reserve(ndc.vertices().size());
1519 if (texBufIndex >= texBufs.size()) texBufs.emplace_back();
1520 vkb::HostVertexBuffer &vb = texBufs[texBufIndex++];
1521 vb.allocate<TexturedVertex>(frameToken(),
device, gpuVerts);
1523 if (
tb.effect == TexturedBatch::Effect::SceneColorDistortion) {
1524 if (!distortionPipe)
return;
1525 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, distortionPipe);
1526 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, texPipelineLayout, 0, 1, &texSet, 0,
nullptr);
1527 }
else if (
tb.effect == TexturedBatch::Effect::DisplayEncode) {
1528 if (!tonemapPipe)
return;
1529 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, tonemapPipe);
1530 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, texPipelineLayout, 0, 1, &texSet, 0,
1532 }
else if (
tb.shader &&
tb.shader->gpuHandle) {
1533 auto *gs =
static_cast<GpuShader *
>(
tb.shader->gpuHandle);
1534 vk::Pipeline customPipeline =
nullptr;
1535 if (presentComposeActive_) {
1536 ensureShaderHdrOffscreenPipeline(
tb.shader);
1538 ? gs->hdrOffscreenOpaquePipeline
1539 : gs->hdrOffscreenPipeline;
1542 ? gs->swapchainOpaquePipeline
1543 : gs->swapchainPipeline;
1545 if (!customPipeline)
return;
1546 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, customPipeline);
1547 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, shaderPipelineLayout, 0, 1,
1548 &texSet, 0,
nullptr);
1549 cb.pushConstants(shaderPipelineLayout,
1550 vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, 0,
1553 vk::Pipeline pipe = toneMapScene ? tonemapPipe : swapchainTexPipe(
tb.blend);
1555 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, pipe);
1556 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, texPipelineLayout, 0, 1,
1557 &texSet, 0,
nullptr);
1559 vk::DeviceSize
offset = 0;
1560 cb.bindVertexBuffers(0, 1, vb, &
offset);
1561 cb.draw(uint32_t(gpuVerts.size()), 1, 0, 0);
1564 std::vector<bool> solidUploaded(solid.size(),
false);
1565 auto uploadSolid = [&](
size_t idx) {
1566 if (
idx >= solid.size() || solidUploaded[
idx] || solid[
idx].batch.empty())
return;
1567 vk::Pipeline pipe = swapchainSolidPipe(solid[
idx].
blend);
1569 Batcher ndc = solid[
idx].batch;
1571 std::vector<ColorVertex> gpuVerts;
1572 gpuVerts.reserve(ndc.vertices().size());
1574 gpuVerts.push_back(ColorVertex{
v.
pos,
v.
color});
1575 if (solidBufs.size() <=
idx) solidBufs.resize(
idx + 1);
1576 solidBufs[
idx].allocate<ColorVertex>(frameToken(),
device, gpuVerts);
1577 solidUploaded[
idx] =
true;
1580 auto drawSolidSpan = [&](uint32_t batchIndex, uint32_t
begin, uint32_t
count) {
1581 if (batchIndex >= solid.size() ||
count == 0 || solid[batchIndex].batch.empty())
return;
1582 vk::Pipeline pipe = swapchainSolidPipe(solid[batchIndex].
blend);
1584 uploadSolid(batchIndex);
1585 vk::DeviceSize
offset = 0;
1586 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, pipe);
1587 cb.bindVertexBuffers(0, 1, solidBufs[batchIndex], &
offset);
1591 auto replaySpans = [&](
const std::vector<OverlaySpan> &
list) {
1592 for (
const auto &sp :
list) {
1593 if (sp.kind == OverlayKind::Solid && sp.index < solid.size() && sp.vertCount > 0) {
1594 drawSolidSpan(sp.index, sp.vertBegin, sp.vertCount);
1595 }
else if (sp.kind == OverlayKind::Textured && (texPipeline || hdrOffscreenTexPipeline) &&
1596 sp.index < textured.size()) {
1597 drawTextured(textured[sp.index]);
1598 }
else if (sp.kind == OverlayKind::Lit && litAvailable && sp.index < lit.size()) {
1599 std::vector<LitBatch> one;
1600 one.push_back(std::move(lit[sp.index]));
1601 drawLitBatches(cb,
width,
height, one, texBufs, texBufIndex,
false);
1602 }
else if (sp.kind == OverlayKind::GpuParticles && sp.index < gpuParticleDraws.size()) {
1603 drawGpuParticleRequest(cb, gpuParticleDraws[sp.index]);
1608 bool engineDrawn =
false;
1609 auto drawEngine3D = [&]() {
1610 if (engineDrawn)
return;
1612 replaySpans(engineSpans);
1615 auto drawPlacedSceneResolve = [&](TexturedBatch &
placed) {
1616 if (!sceneResolve) {
1620 TexturedBatch blit = *sceneResolve;
1621 const glm::vec4 acesColor = sceneResolve->batch.vertices().empty()
1622 ? glm::vec4(1.f, 1.f, 1.f, 65536.f)
1624 const auto &src =
placed.batch.vertices();
1625 if (src.size() >= 6) {
1626 const float x = src[0].pos.x;
1627 const float y = src[0].pos.y;
1628 const float w = src[1].pos.x -
x;
1629 const float h = src[2].pos.y -
y;
1630 const glm::vec4
tint = src[0].color;
1632 blit.batch.addTexturedRect(
x,
y,
w,
h,
1634 tint.b * acesColor.b, acesColor.a),
1635 src[0].uv.x, src[0].uv.y, src[1].uv.x, src[2].uv.y);
1637 drawTextured(blit,
true);
1642 if (autoScene && sceneResolve) {
1643 drawTextured(*sceneResolve,
true);
1647 if (spans.empty() && engineSpans.empty()) {
1648 for (
size_t i = 0; i < solid.size(); ++i) {
1649 if (!solid[i].batch.empty())
1650 drawSolidSpan(uint32_t(i), 0, uint32_t(solid[i].batch.vertices().size()));
1652 if (texPipeline || hdrOffscreenTexPipeline) {
1653 for (
auto &
tb : textured) drawTextured(
tb);
1655 if (litAvailable) drawLitBatches(cb,
width,
height, lit, texBufs, texBufIndex,
false);
1657 for (
const auto &sp : spans) {
1658 if (sp.kind == OverlayKind::Solid && sp.index < solid.size() && sp.vertCount > 0) {
1659 drawSolidSpan(sp.index, sp.vertBegin, sp.vertCount);
1660 }
else if (sp.kind == OverlayKind::Textured &&
1661 (texPipeline || hdrOffscreenTexPipeline) && sp.index < textured.size()) {
1666 const bool placedScene =
1667 sceneResolve && textured[sp.index].texture == sceneTex &&
1668 textured[sp.index].effect != TexturedBatch::Effect::SceneColorDistortion;
1670 drawPlacedSceneResolve(textured[sp.index]);
1672 drawTextured(textured[sp.index]);
1677 }
else if (sp.kind == OverlayKind::Lit && litAvailable && sp.index < lit.size()) {
1678 std::vector<LitBatch> one;
1679 one.push_back(std::move(lit[sp.index]));
1680 drawLitBatches(cb,
width,
height, one, texBufs, texBufIndex,
false);
1681 }
else if (sp.kind == OverlayKind::GpuParticles && sp.index < gpuParticleDraws.size()) {
1682 drawGpuParticleRequest(cb, gpuParticleDraws[sp.index]);
1687 if (uiResolve) drawTextured(*uiResolve);
1695 VkCommandBuffer raw =
static_cast<VkCommandBuffer
>(cb);
1699 if (presentComposeActive_) {
1700 endPresentComposePass();
1701 beginSwapchainColorPass();
1702 swapchainPassOpen =
true;
1703 auto &presentCb = currentPresentCb();
1704 setViewportAndScissor(presentCb, swapchain.extent.width, swapchain.extent.height);
1705 encodePresentComposeToSwapchain(presentCb);
1708 presentRecording = swapchainPass.endRenderPass();
1710 const bool captured =
1712 writeGpuTimestampEnd();
1713 presentRecording.end().submitAndPresent();
1714 readGpuFrameTiming();
1715 presentRecording = {};
1717 hasPresentedFrame =
true;
1718 readbackReady =
true;
1719 readbackCpuSynced =
false;
1721 hasPendingClear =
false;
1722 swapchainPassOpen =
false;
graphics::Texture * albedo
vk::UniqueSampler sampler
std::vector< Point > vertices
std::weak_ptr< PrimitiveScene > scene
const UnitySourceAsset & source
EVENGINE_API_FOUNDATION public API.
Accumulates solid / textured quads in logical (Y-down) coordinates. Used by RenderSystem; not a publi...
void addTexturedRect(float x, float y, float w, float h, const Color &color, float u0, float v0, float u1, float v1, bool rotatedUV=false)
Adds textured rect.
void addTexturedRectRotated(float cx, float cy, float w, float h, float degrees, const Color &color, float u0, float v0, float u1, float v1, bool rotatedUV=false)
Textured quad rotated degrees (clockwise, screen Y-down) around (cx, cy).
void toNDC(int logicalW, int logicalH)
To ndc.
const std::vector< BatchVertex > & vertices() const
Vertices.
virtual void clear(std::optional< Color > color, std::optional< int > stencil, std::optional< double > depth)=0
Clears .
bool screenReadbackEnabled
virtual image::ImageData * readGBufferToImageData(const std::string &attachment)
Read a G-buffer attachment back to CPU as RGBA8. name is one of "depth" (RGBA8 linear depth),...
virtual void drawTexturedRectShader4(Texture *color, Texture *depth, Texture *motion, Texture *extra, Shader *shader, float x, float y, float w, float h, const Color &tint)=0
Post draw with four sampled textures at bindings 0, 1, 2 and 3.
virtual Shader * newMeshShaderFromSpv(const std::vector< uint32_t > &vertSpv, const std::vector< uint32_t > &fragSpv)=0
Create a Mesh3D custom shader (MeshVertex + Frame UBO + albedo). Empty vert → default mesh3d....
int getWidth() const
Returns the width.
virtual Shader * newHairShaderFromWgsl(const std::string &vertWgsl, const std::string &fragWgsl)=0
Create an alpha-blended hair/card shader from WGSL on WebGPU.
virtual Shader * newShaderFromWgsl(const std::string &vertWgsl, const std::string &fragWgsl)=0
Create a 2D custom shader from WGSL source (WebGPU backend). Empty vert → default textured vertex sha...
virtual void drawTexturedRectShaderUV(Texture *texture, Shader *shader, float x, float y, float w, float h, float u0, float v0, float u1, float v1, const Color &color, bool rotatedUV=false, BlendMode blend=BlendMode::Alpha)=0
UV draw with an explicit Shader (nullptr = default textured pipeline).
virtual Shader * newMeshShader(const std::string &vertGlsl, const std::string &fragGlsl)=0
Creates a mesh shader. @ownership Caller deletes unless documented otherwise.
virtual void drawTexturedRectLitUVRotated(Texture *albedo, Texture *normal, float cx, float cy, float w, float h, float degrees, float u0, float v0, float u1, float v1, const Color &color, BlendMode blend=BlendMode::Alpha)=0
Lit 2D draw rotated degrees clockwise (screen Y-down) around (cx, cy). Fragment tangent frame is rebu...
bool recordingEngine3D_
True while RenderSystem3D is submitting (AO / engine overlays).
virtual void setLighting2D(const Lighting2DUBO &ubo)=0
Upload per-frame / per-canvas 2D lighting constants for subsequent lit draws.
virtual image::ImageData * readDecalLayerToImageData(const std::string &attachment)
Read back a DecalLayer attachment ("albedo" | "normal" | "params") to CPU. Renders the pending G-buff...
virtual SceneColorDistortionStatus drawSceneColorDistortionUVRotated(Texture *displacement, float cx, float cy, float w, float h, float degrees, float u0, float v0, float u1, float v1, float strengthPixels, float opacity, bool rotatedUV=false)
Draws scene color distortion uv rotated.
SceneColorDistortionStatus
Refract the resolved 3D scene color through a displacement texture.
virtual Canvas * getCanvas() const =0
Returns the canvas.
virtual bool isDisplayHdrActive() const
True when the live swapchain is scRGB or HDR10.
virtual void drawSolidRect(float x, float y, float w, float h, float r, float g, float b, float a=1.f)
RGBA-float overload matching the script-facing drawSolidRect name.
void ensureFileTexturesReady()
Finish CPU decode and GPU upload for outstanding newTextureFromFile results. @thread Game/render thre...
virtual Shader * newShader(const std::string &vertGlsl, const std::string &fragGlsl)=0
Compile GLSL source with the engine's build-time GLSL compiler. Empty vertGlsl → default textured ver...
virtual void drawTexturedRectShaderUVRotated(Texture *texture, Shader *shader, float cx, float cy, float w, float h, float degrees, float u0, float v0, float u1, float v1, const Color &color, bool rotatedUV=false, BlendMode blend=BlendMode::Alpha)=0
UV draw rotated degrees clockwise (screen Y-down) around the rect center. texture may be null → solid...
virtual Texture * newTexture(int width, int height, const uint8_t *rgba, bool repeatU=false, bool repeatV=false)=0
Creates a texture. @ownership Caller deletes unless documented otherwise.
void setCanvas()
Sets the canvas.
void * presentOverlayUser_
virtual Shader * newHairShaderFromSpv(const std::vector< uint32_t > &vertSpv, const std::vector< uint32_t > &fragSpv)=0
Hair/fur card shader (alpha blend + Kajiya-Kay). Empty vert → mesh3d_hair.vert. Owned by Graphics.
virtual void drawSolidRectRotated(float cx, float cy, float w, float h, float degrees, const Color &color, BlendMode blend=BlendMode::Alpha)=0
Rotated solid quad degrees clockwise (screen Y-down) around (cx, cy).
virtual bool releaseShader(Shader *shader)
Eagerly releases a shader created by this Graphics.
virtual bool isCanvasActive() const =0
True when canvas active.
virtual void drawTexturedRectShaderDepthMotion(Texture *color, Texture *depth, Texture *motion, Shader *shader, float x, float y, float w, float h, const Color &tint)=0
Post draw with color, depth/history and motion/reactive textures.
virtual float getMaxAnisotropy() const =0
Device max supported anisotropy (1 if unsupported). Valid after initWithWindow.
virtual void drawTexturedRectUV(Texture *texture, float x, float y, float w, float h, float u0, float v0, float u1, float v1, const Color &color)=0
Draw a textured sub-rect (atlas / tile UVs). texture may be null → solid.
virtual void drawTexturedRect(Texture *texture, float x, float y, float w, float h, float r, float g, float b, float a=1.f)
RGBA-float overload matching the script-facing drawTexturedRect name.
virtual Shader * newShaderFromSpv(const std::vector< uint32_t > &vertSpv, const std::vector< uint32_t > &fragSpv)=0
Create a custom 2D shader from SPIR-V words (vert + frag). Owned by Graphics. Vertex stage may be emp...
virtual Canvas * newHDRCanvas(int width, int height)=0
Create an engine-internal linear RGBA16F post-process target.
virtual Texture * getSceneColorTexture()
Sampleable 3D color target for the current frame (RGB = lit, A = linear depth). Valid after begin3DFr...
PresentOverlayFn presentOverlayFn_
virtual void drawTexturedRectShader5(Texture *color, Texture *depth, Texture *motion, Texture *extra, Texture *specular, Shader *shader, float x, float y, float w, float h, const Color &tint)=0
Post draw with five sampled textures at bindings 0 through 4.
virtual Canvas * newCanvas(int width, int height)=0
Create an offscreen render target (sampleable). Owned by Graphics.
virtual Shader * newMeshShaderFromWgsl(const std::string &vertWgsl, const std::string &fragWgsl)=0
Create a Mesh3D custom shader from WGSL source (WebGPU backend). The WGSL must declare the engine's F...
virtual void setViewportSize(int width, int height, int pixelwidth, int pixelheight)=0
Sets the current graphics display viewport dimensions.
virtual void drawTexturedRectLitUV(Texture *albedo, Texture *normal, float x, float y, float w, float h, float u0, float v0, float u1, float v1, const Color &color, BlendMode blend=BlendMode::Alpha)=0
Lit 2D draw (albedo + normal map). Uses Lighting2DUBO from setLighting2D. normal may be null → treate...
int getHeight() const
Returns the height.
virtual Shader * newShaderFromSpvFile(const std::string &vertPath, const std::string &fragPath)=0
Load SPIR-V from files via Filesystem (empty vertPath → default textured vert).
virtual void drawTexturedRectShader(Texture *texture, Shader *shader, float x, float y, float w, float h, const Color &color)=0
Draw with an explicit Shader (nullptr = default textured pipeline).
virtual void drawTexturedRectShaderDepth(Texture *color, Texture *depth, Shader *shader, float x, float y, float w, float h, const Color &tint)=0
Fullscreen/post draw sampling color at binding 0 and depth at binding 1 (hardware D32,...
virtual void drawPrimitiveCanvas(const PrimitiveCanvas2D &canvas)=0
Resolves and queues one frame-local primitive Canvas.
Frame-local Skia-style recorder for 2D line primitives.
Declarative, compilable 3D render control.
static constexpr uint32_t kPushConstantBytes
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
EVENGINE_API_BACKENDS public API.
image::ImageData * renderEntityIdMask(const std::vector< eve::graphics::Graphics::EntityIdDraw > &draws, const glm::mat4 &viewProj, int width, int height) override
Renders entity id mask.
Color getPixel(int x, int y) override
Returns the pixel.
image::ImageData * newImageData() override
Creates a image data. @ownership Caller deletes unless documented otherwise.
void clear(std::optional< Color > color, std::optional< int > stencil, std::optional< double > depth) override
Clears .
OffscreenCanvas public API.
Represents raw pixel data.
FilterMode
Mag/min filter for texture sampling.
std::vector< std::uint32_t > compileGlslToSpirv(const std::string &source, GlslStage stage, const std::string &debugName)
Compile GLSL text to SPIR-V words.
ResolvedPrimitiveTriangles resolvePrimitiveStrokes2D(const PrimitiveCanvas2D &canvas, glm::ivec2 viewport)
Resolves 2D stroke segment bodies into clip-space triangles.
eve::Color Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
MipmapMode
Mipmap filter; Disabled turns off mip sampling (maxLod clamped to 0).
eve::BlendMode BlendMode
Compatibility alias for the shared 2D blend mode.
GlslStage
GLSL stage accepted by compileGlslToSpirv.
ControlEdit combo(const char *label, int &selected, const std::vector< const char * > &items)
Shared immediate renderer for indexed combo boxes.
WidgetDesc list(std::string listId, const std::vector< std::string > &items, const std::function< WidgetDesc(const std::string &, int)> &itemFn)
Expand a string list into a Group of item widgets. itemFn(label, index) builds each row; keys default...
BlendMode
Render-neutral 2D blend mode shared by graphics-facing modules.
Lighting2DUBO public API.
Resolved triangle vertex consumed by primitive GPU backends.
One contiguous 2D blend/order batch in the resolved vertex stream.
Sampler state for a Texture (filter, wrap, mip LOD, anisotropy). Defaults match historical engine beh...
vkb::BoundSet descriptorSet
TexturedVertex public API.
Backend-internal textured rectangle queued by a UI presenter.