13#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"
57void Graphics::ensurePresentCaptureHook() {
63 presentModel.after_render_before_present =
nullptr;
66void Graphics::ensureReadbackSlots() {
69 const size_t want = std::max<size_t>(2, frameSlotCount());
70 if (!screenReadbackSlots.empty() && screenReadbackSlots.size() >= want &&
71 screenReadbackBytes == bytes)
76 for (
auto &slot : screenReadbackSlots) {
78 device->unmapMemory(slot.staging.memory);
79 slot.mapped =
nullptr;
81 slot.staging.release();
83 screenReadbackSlots.clear();
84 screenReadbackSlots.resize(want);
85 for (
auto &slot : screenReadbackSlots) {
86 slot.staging = vkb::GenericBuffer(device, vk::BufferUsageFlagBits::eTransferDst,
87 vk::DeviceSize(bytes),
88 vk::MemoryPropertyFlagBits::eHostVisible |
89 vk::MemoryPropertyFlagBits::eHostCoherent);
91 screenReadbackBytes = bytes;
92 readbackReady =
false;
93 readbackCpuSynced =
false;
94 readbackWriteSlot = 0;
97bool Graphics::recordSwapchainReadback(vk::CommandBuffer cb) {
99 if (!presentModel.has_acquired_image)
return false;
100 const vk::Format fmt = swapchain.image_format;
101 const bool bgra = (fmt == vk::Format::eB8G8R8A8Unorm || fmt == vk::Format::eB8G8R8A8Srgb);
102 const bool rgba = (fmt == vk::Format::eR8G8B8A8Unorm || fmt == vk::Format::eR8G8B8A8Srgb);
103 if (!bgra && !rgba)
return false;
106 const uint32_t imageIndex = presentModel.acquired_image_index;
107 auto &images = swapchain.get_images();
108 if (imageIndex >= images.size())
return false;
110 ensureReadbackSlots();
111 if (screenReadbackSlots.empty())
return false;
113 const size_t slot = size_t(presentRecording.slot().index) % screenReadbackSlots.size();
114 readbackWriteSlot = slot;
115 const vk::Image
image = images[imageIndex];
120 vk::ImageMemoryBarrier toTransfer{};
121 toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
122 toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
123 toTransfer.oldLayout = vk::ImageLayout::ePresentSrcKHR;
124 toTransfer.newLayout = vk::ImageLayout::eTransferSrcOptimal;
125 toTransfer.image =
image;
126 toTransfer.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
130 toTransfer.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
131 toTransfer.dstAccessMask = vk::AccessFlagBits::eTransferRead;
132 cb.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput,
133 vk::PipelineStageFlagBits::eTransfer, {}, 0,
nullptr, 0,
nullptr, 1,
136 vk::BufferImageCopy region{};
137 region.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
139 cb.copyImageToBuffer(image, vk::ImageLayout::eTransferSrcOptimal,
140 screenReadbackSlots[slot].staging.buffer, region);
142 vk::ImageMemoryBarrier toPresent = toTransfer;
143 toPresent.oldLayout = vk::ImageLayout::eTransferSrcOptimal;
144 toPresent.newLayout = vk::ImageLayout::ePresentSrcKHR;
145 toPresent.srcAccessMask = vk::AccessFlagBits::eTransferRead;
146 toPresent.dstAccessMask = vk::AccessFlagBits::eMemoryRead;
147 cb.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
148 vk::PipelineStageFlagBits::eBottomOfPipe, {}, 0,
nullptr, 0,
nullptr, 1,
153void Graphics::syncReadbackCpu() {
154 if (!readbackReady || screenReadbackSlots.empty())
return;
155 if (readbackCpuSynced && !lastFrameRgba.empty())
return;
156 if (readbackWriteSlot >= screenReadbackSlots.size())
return;
158 const size_t bytes = screenReadbackBytes;
159 if (bytes == 0)
return;
163 presentModel.waitForFrameSlot(readbackWriteSlot);
165 auto &slot = screenReadbackSlots[readbackWriteSlot];
167 slot.mapped = device->mapMemory(slot.staging.memory, 0, vk::DeviceSize(bytes));
169 lastFrameRgba.resize(bytes);
172 const size_t words = bytes / 8;
173 const uint64_t *src64 =
static_cast<const uint64_t *
>(slot.mapped);
174 uint64_t *dst64 =
reinterpret_cast<uint64_t *
>(lastFrameRgba.data());
175 for (
size_t i = 0; i < words; ++i) {
176 const uint64_t
v = src64[i];
177 dst64[i] = (
v & 0xFF00FF00FF00FF00ull) |
178 ((
v & 0x000000FF000000FFull) << 16) |
179 ((
v >> 16) & 0x000000FF000000FFull);
181 for (
size_t i = words * 8; i < bytes; i += 4) {
182 const uint32_t
v = *
reinterpret_cast<const uint32_t *
>(
183 static_cast<const uint8_t *
>(slot.mapped) + i);
184 const uint32_t out = (
v & 0xFF00FF00u) | ((
v & 0xFFu) << 16) | ((
v >> 16) & 0xFFu);
185 std::memcpy(lastFrameRgba.data() + i, &out, 4);
188 std::memcpy(lastFrameRgba.data(), slot.mapped, bytes);
190 readbackCpuSynced =
true;
193void Graphics::destroyReadbackResources() {
194 for (
auto &slot : screenReadbackSlots) {
196 device->unmapMemory(slot.staging.memory);
197 slot.mapped =
nullptr;
199 slot.staging.release();
201 screenReadbackSlots.clear();
202 screenReadbackBytes = 0;
203 readbackReady =
false;
204 readbackCpuSynced =
false;
205 hasPresentedFrame =
false;
210 if (!hasPresentedFrame || lastFrameRgba.empty())
211 throw Exception(
"Graphics::newImageData: no presented frame");
213 std::memcpy(img->getData(), lastFrameRgba.data(), lastFrameRgba.size());
219 if (!hasPresentedFrame || lastFrameRgba.empty())
220 throw Exception(
"Graphics::getPixel: no presented frame");
222 throw Exception(
"Graphics::getPixel: out of bounds (%d,%d)",
x,
y);
226 const int cx = std::min(std::max(pxX, 0),
pixelWidth - 1);
228 const size_t i = (size_t(
cy) * size_t(
pixelWidth) + size_t(
cx)) * 4;
229 float r = lastFrameRgba[i + 0] / 255.f;
230 float g = lastFrameRgba[i + 1] / 255.f;
231 float b = lastFrameRgba[i + 2] / 255.f;
232 float a = lastFrameRgba[i + 3] / 255.f;
234 const vk::Format fmt = swapchain.image_format;
235 if (fmt == vk::Format::eB8G8R8A8Srgb || fmt == vk::Format::eR8G8B8A8Srgb) {
236 auto toLinear = [](
float u) {
237 return (
u <= 0.04045f) ? (
u / 12.92f) : std::pow((
u + 0.055f) / 1.055f, 2.4f);
247 const std::vector<eve::graphics::Graphics::EntityIdDraw> &draws,
const glm::mat4 &
viewProj,
249 if (!initialized ||
width <= 0 ||
height <= 0)
return nullptr;
253 if (!gbufferPipeline || !gbufferRenderPass)
return nullptr;
254 auto *slot = currentGBufferSlot();
255 if (!slot || !slot->framebuffer || !whiteTexture)
return nullptr;
261 std::vector<GBufferDraw> idDraws;
262 idDraws.reserve(draws.size());
263 auto u8 = [](
float x) -> uint32_t {
264 return uint32_t(std::lround(std::clamp(
x, 0.f, 1.f) * 255.f));
266 for (
const auto &
d : draws) {
267 if (!
d.mesh || !
d.mesh->gpuHandle)
continue;
270 gd.albedo = whiteTexture;
272 gd.push.modelR0 = glm::vec4(
d.model[0][0],
d.model[1][0],
d.model[2][0],
d.model[3][0]);
273 gd.push.modelR1 = glm::vec4(
d.model[0][1],
d.model[1][1],
d.model[2][1],
d.model[3][1]);
274 gd.push.modelR2 = glm::vec4(
d.model[0][2],
d.model[1][2],
d.model[2][2],
d.model[3][2]);
275 const uint32_t packed = u8(
d.idColor.r) | (u8(
d.idColor.g) << 8) |
276 (u8(
d.idColor.b) << 16) | (u8(
d.idColor.a) << 24);
277 gd.push.clip = glm::vec4(0.1f, 100.f, glm::uintBitsToFloat(packed), 0.f);
278 idDraws.push_back(gd);
280 if (idDraws.empty())
return nullptr;
282 const uint32_t
w = uint32_t(
width);
283 const uint32_t
h = uint32_t(
height);
284 const vk::DeviceSize byteSize = vk::DeviceSize(
w) * vk::DeviceSize(
h) * 4;
285 vkb::GenericBuffer staging(device, vk::BufferUsageFlagBits::eTransferDst, byteSize,
286 vk::MemoryPropertyFlagBits::eHostVisible |
287 vk::MemoryPropertyFlagBits::eHostCoherent);
289 vkb::executeImmediately(device.instance, uploadPool, device.getQueue(vkb::QueueType::graphics),
290 [&](vk::CommandBuffer cb) {
291 std::array<vk::ClearValue, 4> clears{};
292 clears[0].color = vk::ClearColorValue(std::array<float, 4>{0, 0, 0, 0});
293 clears[1].color = vk::ClearColorValue(std::array<float, 4>{0, 0, 0, 0});
294 clears[2].color = vk::ClearColorValue(std::array<float, 4>{0, 0, 0, 0});
295 clears[3].depthStencil = vk::ClearDepthStencilValue{1.0f, 0};
296 vk::RenderPassBeginInfo rpBegin{};
297 rpBegin.renderPass = gbufferRenderPass;
298 rpBegin.framebuffer = slot->framebuffer;
299 rpBegin.renderArea = vk::Rect2D{{0, 0}, {
w,
h}};
300 rpBegin.clearValueCount = uint32_t(clears.size());
301 rpBegin.pClearValues = clears.data();
302 slot->normal.beginColorAttachment();
303 slot->depthColor.beginColorAttachment();
304 slot->albedo.beginColorAttachment();
305 slot->depth.beginDepthAttachment();
306 cb.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
307 setViewportAndScissor(cb,
w,
h);
308 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, gbufferPipeline);
309 for (
const auto &
d : idDraws) {
310 auto *gpuMesh =
static_cast<GpuMesh *
>(
d.mesh->gpuHandle);
311 if (!gpuMesh)
continue;
312 if (whiteTexture && whiteTexture->
gpuHandle && texSetLayout) {
314 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
315 gbufferPipelineLayout, 0, 1,
316 gpuTex->descriptorSet.ptr(), 0,
nullptr);
318 cb.pushConstants(gbufferPipelineLayout,
319 vk::ShaderStageFlagBits::eVertex |
320 vk::ShaderStageFlagBits::eFragment,
321 0,
sizeof(GBufferPush), &
d.push);
322 drawIndexedMesh(cb, *gpuMesh);
325 slot->normal.endSampledLayout();
326 slot->depthColor.endSampledLayout();
327 slot->albedo.endSampledLayout();
328 slot->depth.endSampledLayout();
331 slot->albedo.setLayout(cb, vk::ImageLayout::eTransferSrcOptimal);
332 vk::BufferImageCopy region{};
333 region.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
334 region.imageExtent = vk::Extent3D{
w,
h, 1};
335 cb.copyImageToBuffer(slot->albedo.image(),
336 vk::ImageLayout::eTransferSrcOptimal, staging.buffer,
338 slot->albedo.setLayout(cb, vk::ImageLayout::eShaderReadOnlyOptimal);
342 void *mapped =
device->mapMemory(staging.memory, 0, byteSize);
343 std::memcpy(img->getData(), mapped,
size_t(byteSize));
344 device->unmapMemory(staging.memory);
351 rc->getGBuffer()->setTargets(
int(
w),
int(
h), &slot->depthColorTex, &slot->normalTex,
352 &slot->albedoTex, &slot->depthTex);
358 if (!initialized)
return nullptr;
359 auto *slot = currentGBufferSlot();
360 if (!slot)
return nullptr;
361 vkb::ColorTarget *src =
nullptr;
363 src = &slot->depthColor;
364 else if (
name ==
"normal")
366 else if (
name ==
"albedo")
371 const uint32_t
w = uint32_t(gbufferWidth);
372 const uint32_t
h = uint32_t(gbufferHeight);
373 if (
w == 0 ||
h == 0)
return nullptr;
375 const vk::DeviceSize byteSize = vk::DeviceSize(
w) * vk::DeviceSize(
h) * 4;
376 vkb::GenericBuffer staging(
device, vk::BufferUsageFlagBits::eTransferDst, byteSize,
377 vk::MemoryPropertyFlagBits::eHostVisible |
378 vk::MemoryPropertyFlagBits::eHostCoherent);
379 vkb::executeImmediately(
device.instance, uploadPool,
device.getQueue(vkb::QueueType::graphics),
380 [&](vk::CommandBuffer cb) {
381 src->setLayout(cb, vk::ImageLayout::eTransferSrcOptimal);
382 vk::BufferImageCopy region{};
383 region.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
384 region.imageExtent = vk::Extent3D{
w,
h, 1};
385 cb.copyImageToBuffer(src->image(), vk::ImageLayout::eTransferSrcOptimal,
386 staging.buffer, region);
387 src->setLayout(cb, vk::ImageLayout::eShaderReadOnlyOptimal);
391 void *mapped =
device->mapMemory(staging.memory, 0, byteSize);
392 std::memcpy(img->getData(), mapped,
size_t(byteSize));
393 device->unmapMemory(staging.memory);
402 if (!initialized)
throw Exception(
"newCanvas: graphics not initialized");
403 if (
w <= 0 ||
h <= 0)
throw Exception(
"newCanvas: invalid size");
404 ensureOffscreenPipelines();
405 auto c = std::make_unique<OffscreenCanvas>(
this,
w,
h);
407 ownedCanvases.push_back(std::move(
c));
413 if (next ==
static_cast<Canvas *
>(
this)) next =
nullptr;
414 if (next == activeCanvas)
return;
415 bool hasSolid =
false;
416 for (
const auto &sb : solidBatches)
417 if (!sb.batch.empty()) hasSolid =
true;
418 if (hasSolid || !texturedBatches.empty()) flushBatch();
423 return activeCanvas !=
nullptr;
427 return activeCanvas ? activeCanvas :
const_cast<Graphics *
>(
this);
436 if (initialized && changed) swapchainDirty =
true;
439void Graphics::clear2DBatches() {
440 solidBatches.
clear();
441 texturedBatches.clear();
443 overlaySpans.clear();
444 engine3DSpans.clear();
445 pendingSceneResolve.reset();
446 pendingUiResolve.reset();
447 sceneColorComposited =
false;
450void Graphics::noteSolidOverlay() {
451 if (solidBatches.empty())
return;
452 const uint32_t
idx = uint32_t(solidBatches.size() - 1);
453 const uint32_t
n = uint32_t(solidBatches.back().batch.vertices().size());
455 if (!spans.empty() && spans.back().kind == OverlayKind::Solid &&
456 spans.back().index ==
idx) {
457 spans.back().vertCount =
n - spans.back().vertBegin;
460 const uint32_t begin =
n >= 6u ?
n - 6u : 0
u;
461 spans.push_back({OverlayKind::Solid,
idx, begin,
n - begin});
464void Graphics::noteTexturedOverlay(Texture *tex) {
467 const uint32_t
idx = texturedBatches.empty() ? 0
u : uint32_t(texturedBatches.size() - 1);
468 if (!spans.empty() && spans.back().kind == OverlayKind::Textured && spans.back().index ==
idx)
470 spans.push_back({OverlayKind::Textured,
idx, 0, 0});
473void Graphics::noteLitOverlay() {
475 const uint32_t
idx = litBatches.empty() ? 0
u : uint32_t(litBatches.size() - 1);
476 if (!spans.empty() && spans.back().kind == OverlayKind::Lit && spans.back().index ==
idx)
478 spans.push_back({OverlayKind::Lit,
idx, 0, 0});
483 if (
frameHad3D && activeCanvas ==
nullptr)
return;
485 hasPendingClear =
true;
488 oc->clear(
clearColor, std::nullopt, std::nullopt);
494 auto it = std::find_if(solidBatches.begin(), solidBatches.end(),
495 [&](
const SolidBatch &sb) { return sb.blend == blend; });
496 if (it == solidBatches.end()) {
497 solidBatches.push_back(SolidBatch{blend,
Batcher{}});
498 it = solidBatches.end() - 1;
506 auto it = std::find_if(solidBatches.begin(), solidBatches.end(),
507 [&](
const SolidBatch &sb) { return sb.blend == blend; });
508 if (it == solidBatches.end()) {
509 solidBatches.push_back(SolidBatch{blend,
Batcher{}});
510 it = solidBatches.end() - 1;
519vk::Sampler Graphics::createVkSampler(
const TextureSampler &sampler, uint32_t mipLevels)
const {
520 auto wrapMode = [](
bool repeat) {
521 return repeat ? vk::SamplerAddressMode::eRepeat : vk::SamplerAddressMode::eClampToEdge;
528 : vk::SamplerMipmapMode::eLinear;
532 float maxLod = useMips ? std::min(sampler.
maxLod,
float(mipLevels - 1)) : 0.f;
536 bool enableAniso =
false;
537 if (sampler.
maxAnisotropy > 1.f && maxSamplerAnisotropy > 1.f) {
539 aniso = std::min(sampler.
maxAnisotropy, maxSamplerAnisotropy);
542 vkb::SamplerBuilder sb;
543 return sb.magFilter(toFilter(sampler.
mag))
544 .minFilter(toFilter(sampler.
min))
545 .mipmapMode(useMips ? toMip(sampler.
mipmap) : vk::SamplerMipmapMode::eNearest)
546 .addressModeU(wrapMode(sampler.
repeatU))
547 .addressModeV(wrapMode(sampler.
repeatV))
548 .addressModeW(wrapMode(sampler.repeatW))
549 .mipLodBias(sampler.lodBias)
550 .anisotropyEnable(enableAniso ? VK_TRUE : VK_FALSE)
551 .maxAnisotropy(aniso)
552 .minLod(useMips ? sampler.minLod : 0.f)
557void Graphics::writeCombinedImageDescriptor(GpuTexture *gpu) {
558 if (!gpu || !gpu->descriptorSet || !gpu->sampler)
return;
559 vk::ImageView
view = gpu->imageView();
561 vkb::UnboundSet unbound = vkb::UnboundSet::reopenAfterIdle(gpu->descriptorSet);
562 vkb::DescriptorSetUpdater updater;
563 updater.beginDescriptorSet(unbound)
564 .beginImages(0, 0, vk::DescriptorType::eCombinedImageSampler)
565 .image(vkb::SampledImage::forLaterSample(gpu->sampler,
view))
566 .beginImages(1, 0, vk::DescriptorType::eCombinedImageSampler)
567 .image(vkb::SampledImage::forLaterSample(gpu->sampler,
view))
569 gpu->descriptorSet = std::move(unbound).publish();
583 ASSERT(rgba !=
nullptr);
584 if (!initialized)
throw Exception(
"newTexture: graphics not initialized");
585 if (
w <= 0 ||
h <= 0 || !rgba)
throw Exception(
"newTexture: invalid args");
588 const uint32_t mipLevels =
591 auto gpu = std::make_unique<GpuTexture>();
595 gpu->mipLevels = mipLevels;
596 gpu->samplerState = info.
sampler;
597 gpu->image = vkb::TextureImage2D(
device, uint32_t(
w), uint32_t(
h), mipLevels);
599 std::vector<uint8_t> bytes =
600 (mipLevels > 1) ? buildMipChain2D(rgba, uint32_t(
w), uint32_t(
h), mipLevels)
601 : std::vector<uint8_t>(rgba, rgba +
size_t(
w) *
size_t(
h) * 4);
602 gpu->image.upload(uploadPool,
device.getQueue(vkb::QueueType::graphics), bytes);
604 gpu->sampler = createVkSampler(info.
sampler, mipLevels);
606 auto sets = vkb::DescriptorSetBuilder().layout(texSetLayout).build(
device.instance, descriptorPool);
608 gpu->descriptorSet = vkb::BoundSet{sets[0]};
609 writeCombinedImageDescriptor(gpu.get());
611 auto tex = std::make_unique<Texture>();
615 tex->pixelHeight =
h;
616 tex->mipmapCount = int(mipLevels);
618 tex->gpuHandle = gpu.get();
621 ownedTextures.push_back(std::move(tex));
622 ownedGpuTextures.push_back(std::move(gpu));
634 ASSERT_GT(faceSize, 0);
635 ASSERT(rgbaFaces !=
nullptr);
636 if (!initialized)
throw Exception(
"newCubemap: graphics not initialized");
637 if (faceSize <= 0 || !rgbaFaces)
throw Exception(
"newCubemap: invalid args");
643 const uint32_t mipLevels =
646 const size_t faceBytes = size_t(faceSize) * size_t(faceSize) * 4u;
647 auto gpu = std::make_unique<GpuTexture>();
648 gpu->width = faceSize;
649 gpu->height = faceSize;
651 gpu->mipLevels = mipLevels;
652 gpu->samplerState = info.
sampler;
653 gpu->cubeImage = vkb::TextureImageCube(
device,
device.physical_device.memory_properties,
654 uint32_t(faceSize), uint32_t(faceSize), mipLevels);
656 std::vector<uint8_t> bytes =
657 (mipLevels > 1) ? buildMipChainCube(rgbaFaces, uint32_t(faceSize), mipLevels)
658 : std::vector<uint8_t>(rgbaFaces, rgbaFaces + faceBytes * 6u);
659 gpu->cubeImage.upload(uploadPool,
device.getQueue(vkb::QueueType::graphics), bytes);
661 gpu->sampler = createVkSampler(info.
sampler, mipLevels);
664 auto tex = std::make_unique<Texture>();
665 tex->width = faceSize;
666 tex->height = faceSize;
667 tex->pixelWidth = faceSize;
668 tex->pixelHeight = faceSize;
670 tex->mipmapCount = int(mipLevels);
672 tex->gpuHandle = gpu.get();
675 ownedTextures.push_back(std::move(tex));
676 ownedGpuTextures.push_back(std::move(gpu));
681 ASSERT(
data !=
nullptr);
683 if (
data->getFormat() !=
"RGBA8")
684 throw Exception(
"newTexture: only RGBA8 ImageData supported for now");
686 static_cast<const uint8_t *
>(
data->getData()));
690 ASSERT(
data !=
nullptr);
692 if (
data->getFormat() !=
"RGBA8")
693 throw Exception(
"newTexture: only RGBA8 ImageData supported for now");
695 static_cast<const uint8_t *
>(
data->getData()), info);
700 if (!texture || !texture->
gpuHandle || !initialized)
return;
701 for (
auto &owned : ownedGpuTextures) {
702 if (owned.get() != texture->
gpuHandle)
continue;
704 waitForSharedGpuResources();
705 if (owned->sampler)
device->destroySampler(owned->sampler);
706 owned->samplerState = sampler;
707 owned->sampler = createVkSampler(sampler, owned->mipLevels);
709 if (!owned->isCube) writeCombinedImageDescriptor(owned.get());
710 invalidateTextureBindings();
716 if (!texture || !texture->
gpuHandle)
return false;
718 if (texture == whiteTexture || texture == flatNormalTexture ||
719 texture == flatNormalTexture3D || texture == defaultEnvCubemap)
723 auto gpuIt = std::find_if(ownedGpuTextures.begin(), ownedGpuTextures.end(),
724 [&](
const std::unique_ptr<GpuTexture> &g) {
725 return g.get() == gpu;
727 if (gpuIt == ownedGpuTextures.end())
return false;
729 auto texIt = std::find_if(ownedTextures.begin(), ownedTextures.end(),
730 [&](
const std::unique_ptr<Texture> &t) {
731 return t.get() == texture;
733 if (texIt == ownedTextures.end())
return false;
736 for (
auto it = texturesByPath.begin(); it != texturesByPath.end();) {
737 if (it->second == texture)
738 it = texturesByPath.erase(it);
744 waitForSharedGpuResources();
745 if ((*gpuIt)->sampler)
device->destroySampler((*gpuIt)->sampler);
747 ownedGpuTextures.erase(gpuIt);
749 (void)texIt->release();
750 ownedTextures.erase(texIt);
755 if (!tex || !
data)
return false;
756 if (
data->getFormat() !=
"RGBA8")
return false;
757 const int w =
data->getWidth();
758 const int h =
data->getHeight();
759 const auto *rgba =
static_cast<const uint8_t *
>(
data->getData());
760 if (
w <= 0 ||
h <= 0 || !rgba)
return false;
765 info = normalizeTextureInfo(info);
766 const uint32_t mipLevels =
769 auto gpu = std::make_unique<GpuTexture>();
773 gpu->mipLevels = mipLevels;
774 gpu->samplerState = info.
sampler;
775 gpu->image = vkb::TextureImage2D(
device, uint32_t(
w), uint32_t(
h), mipLevels);
776 std::vector<uint8_t> bytes =
777 (mipLevels > 1) ? buildMipChain2D(rgba, uint32_t(
w), uint32_t(
h), mipLevels)
778 : std::vector<uint8_t>(rgba, rgba +
size_t(
w) *
size_t(
h) * 4);
779 gpu->image.upload(uploadPool,
device.getQueue(vkb::QueueType::graphics), bytes);
781 gpu->sampler = createVkSampler(info.
sampler, mipLevels);
783 auto sets = vkb::DescriptorSetBuilder().layout(texSetLayout).build(
device.instance, descriptorPool);
785 gpu->descriptorSet = vkb::BoundSet{sets[0]};
786 writeCombinedImageDescriptor(gpu.get());
789 for (
auto &owned : ownedGpuTextures) {
790 if (owned.get() != oldHandle)
continue;
793 waitForSharedGpuResources();
794 if (owned->sampler)
device->destroySampler(owned->sampler);
795 owned = std::move(gpu);
803 invalidateTextureBindings();
815 ownedGpuTextures.push_back(std::move(gpu));
820 ASSERT(!filename.empty());
821 if (filename.empty())
throw Exception(
"newTextureFromFile: empty filename");
823 const std::string key = normalizeTexPath(filename);
824 auto *imgMod = image::Image::create();
827 auto it = texturesByPath.find(key);
828 if (it != texturesByPath.end() && it->second) {
829 if (!replaceTexturePixels(it->second,
data.get()))
830 throw Exception(
"newTextureFromFile: reload failed '%s'", filename.c_str());
835 texturesByPath[key] = tex;
840 if (filename.empty())
return false;
841 const std::string key = normalizeTexPath(filename);
842 auto it = texturesByPath.find(key);
843 if (it == texturesByPath.end() || !it->second)
return false;
847 auto *imgMod = image::Image::create();
853 if (!
data)
return false;
854 return replaceTexturePixels(it->second,
data);
863 drawTexturedRectShaderUV(texture,
shader,
x,
y,
w,
h, 0.f, 0.f, 1.f, 1.f,
color);
867 float v0,
float u1,
float v1,
const Color &
color) {
868 drawTexturedRectShaderUV(texture,
currentShader,
x,
y,
w,
h, u0, v0, u1, v1,
color);
872 float h,
float u0,
float v0,
float u1,
float v1,
878 if (texturedBatches.empty() || texturedBatches.back().texture != texture ||
879 texturedBatches.back().
depth !=
nullptr ||
880 texturedBatches.back().shader !=
shader ||
881 texturedBatches.back().blend != blend) {
882 texturedBatches.push_back(TexturedBatch{texture,
nullptr,
shader, blend,
Batcher{}});
884 texturedBatches.back().batch.addTexturedRect(
x,
y,
w,
h,
color, u0, v0, u1, v1, rotatedUV);
885 noteTexturedOverlay(texture);
889 float w,
float h,
float degrees,
float u0,
float v0,
896 if (texturedBatches.empty() || texturedBatches.back().texture != texture ||
897 texturedBatches.back().
depth !=
nullptr ||
898 texturedBatches.back().shader !=
shader ||
899 texturedBatches.back().blend != blend) {
900 texturedBatches.push_back(TexturedBatch{texture,
nullptr,
shader, blend,
Batcher{}});
902 texturedBatches.back().batch.addTexturedRectRotated(
cx,
cy,
w,
h,
degrees,
color, u0, v0, u1, v1,
904 noteTexturedOverlay(texture);
908 float y,
float w,
float h,
const Color &tint) {
917 if (texturedBatches.empty() || texturedBatches.back().texture !=
color ||
918 texturedBatches.back().depth !=
depth || texturedBatches.back().shader !=
shader) {
919 texturedBatches.push_back(
922 texturedBatches.back().batch.addTexturedRect(
x,
y,
w,
h, tint, 0.f, 0.f, 1.f, 1.f);
923 noteTexturedOverlay(
color);
928void Graphics::ensureFlatNormalTexture() {
929 if (flatNormalTexture)
return;
930 const uint8_t
px[4] = {128, 128, 255, 255};
935 float h,
float u0,
float v0,
float u1,
float v1,
941 ensureFlatNormalTexture();
943 if (litBatches.empty() || litBatches.back().albedo !=
albedo ||
944 litBatches.back().normal !=
normal) {
947 litBatches.back().batch.addTexturedRect(
x,
y,
w,
h,
color, u0, v0, u1, v1);
952 ASSERT(
albedo !=
nullptr);
953 ASSERT(
normal !=
nullptr);
954 auto &sets = offscreen ? offscreenLit2dSets : currentLit2dSets();
955 vkb::GenericBuffer &ubo = offscreen ? offscreenLighting2dUbo : currentLighting2dUbo();
957 auto it = sets.find(key);
958 if (it != sets.end())
return it->second;
960 vk::DescriptorSetAllocateInfo alloc{};
961 alloc.descriptorPool = descriptorPool;
962 alloc.descriptorSetCount = 1;
963 alloc.pSetLayouts = &lit2dSetLayout;
964 vkb::UnboundSet unbound{
device->allocateDescriptorSets(alloc).front()};
966 vkb::DescriptorSetUpdater updater;
967 updater.beginDescriptorSet(unbound)
968 .beginImages(0, 0, vk::DescriptorType::eCombinedImageSampler)
969 .image(vkb::SampledImage::forLaterSample(
albedo->sampler,
albedo->image.imageView()))
970 .beginImages(1, 0, vk::DescriptorType::eCombinedImageSampler)
971 .image(vkb::SampledImage::forLaterSample(
normal->sampler,
normal->image.imageView()))
972 .beginBuffers(2, 0, vk::DescriptorType::eUniformBuffer)
973 .buffer(ubo.buffer, 0,
sizeof(Lighting2DUBO))
976 vkb::BoundSet bound = std::move(unbound).publish();
977 sets.emplace(key, bound);
981vkb::BoundSet Graphics::post2SetFor(GpuTexture *
color, GpuTexture *
depth) {
983 vk::ImageView colorView =
color->imageView();
984 if (!colorView)
return {};
987 auto it = post2Sets.find(key);
988 if (it != post2Sets.end())
return it->second;
990 auto sets = vkb::DescriptorSetBuilder().layout(texSetLayout).build(
device.instance, descriptorPool);
992 vkb::UnboundSet unbound{sets[0]};
993 vkb::DescriptorSetUpdater updater;
994 updater.beginDescriptorSet(unbound)
995 .beginImages(0, 0, vk::DescriptorType::eCombinedImageSampler)
996 .image(vkb::SampledImage::forLaterSample(
color->sampler, colorView))
997 .beginImages(1, 0, vk::DescriptorType::eCombinedImageSampler)
998 .image(vkb::SampledImage::forLaterSample(
depth->sampler,
depth->imageView()))
1000 vkb::BoundSet bound = std::move(unbound).publish();
1001 post2Sets.emplace(key, bound);
1005void Graphics::drawLitBatches(vk::CommandBuffer cb,
int viewW,
int viewH, vk::Pipeline pipeline,
1006 std::vector<LitBatch> &batches,
1007 std::vector<vkb::HostVertexBuffer> &texBufs,
size_t &texBufIndex,
1009 if (!pipeline || batches.empty() || !lit2dPipelineLayout)
return;
1010 lighting2dFrame.meta.y = float(viewW);
1011 lighting2dFrame.meta.z = float(viewH);
1012 vkb::GenericBuffer &ubo = offscreen ? offscreenLighting2dUbo : currentLighting2dUbo();
1013 ubo.updateLocal(frameToken(), &lighting2dFrame,
sizeof(Lighting2DUBO));
1015 for (
auto &lb : batches) {
1016 if (lb.batch.empty() || !lb.albedo || !lb.albedo->gpuHandle)
continue;
1017 ensureFlatNormalTexture();
1018 Texture *ntex = lb.normal ? lb.normal : flatNormalTexture;
1019 if (!ntex || !ntex->gpuHandle)
continue;
1020 auto *albedoGpu =
static_cast<GpuTexture *
>(lb.albedo->gpuHandle);
1021 auto *normalGpu =
static_cast<GpuTexture *
>(ntex->gpuHandle);
1023 Batcher ndc = lb.batch;
1024 ndc.toNDC(viewW, viewH);
1025 std::vector<TexturedVertex> gpuVerts;
1026 gpuVerts.reserve(ndc.vertices().size());
1027 for (
const auto &
v : ndc.vertices())
1028 gpuVerts.push_back(TexturedVertex{
v.
pos,
v.
color,
v.uv});
1030 if (texBufIndex >= texBufs.size()) texBufs.emplace_back();
1031 vkb::HostVertexBuffer &vb = texBufs[texBufIndex++];
1032 vb.allocate<TexturedVertex>(frameToken(),
device, gpuVerts);
1034 vk::DescriptorSet set = lit2dSetFor(albedoGpu, normalGpu, offscreen);
1035 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline);
1036 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, lit2dPipelineLayout, 0, 1, &set, 0,
1038 vk::DeviceSize offset = 0;
1039 cb.bindVertexBuffers(0, 1, vb, &offset);
1040 cb.draw(uint32_t(gpuVerts.size()), 1, 0, 0);
1046 const std::vector<uint32_t> &fragSpv) {
1047 ASSERT(initialized);
1048 if (!initialized)
throw Exception(
"newShaderFromSpv: graphics not initialized");
1049 if (fragSpv.empty())
throw Exception(
"newShaderFromSpv: empty fragment SPIR-V");
1051 std::vector<uint32_t>
vert = vertSpv;
1053 vert.assign(textured_vert_spv, textured_vert_spv + textured_vert_spv_count);
1054 if (
vert[0] != 0x07230203 || fragSpv[0] != 0x07230203)
1055 throw Exception(
"newShaderFromSpv: SPIR-V magic mismatch");
1057 auto gpu = std::make_unique<GpuShader>();
1058 gpu->pipelineLayout = shaderPipelineLayout;
1059 gpu->swapchainPipeline =
1060 createTexturedStylePipeline(
vert, fragSpv, renderpass, shaderPipelineLayout);
1061 if (offscreenRenderPass) {
1062 gpu->offscreenPipeline =
1063 createTexturedStylePipeline(
vert, fragSpv, offscreenRenderPass, shaderPipelineLayout);
1066 auto sh = std::make_unique<Shader>();
1067 sh->setSpirv(std::move(
vert), fragSpv);
1068 sh->gpuHandle = gpu.get();
1071 ownedShaders.push_back(std::move(sh));
1072 ownedGpuShaders.push_back(std::move(gpu));
1077 if (fragPath.empty())
throw Exception(
"newShaderFromSpvFile: empty fragPath");
1078 std::vector<uint32_t>
vert;
1079 if (!vertPath.empty())
vert = readSpirvFile(vertPath);
1080 auto frag = readSpirvFile(fragPath);
1085 if (fragGlsl.empty())
throw Exception(
"newShader: empty fragment GLSL");
1086 std::vector<uint32_t>
vert;
1087 if (!vertGlsl.empty())
vert = compileGlslWithGlslc(vertGlsl,
"vert");
1088 auto frag = compileGlslWithGlslc(fragGlsl,
"frag");
1093 const std::vector<uint32_t> &fragSpv) {
1094 ASSERT(initialized);
1095 if (!initialized)
throw Exception(
"newMeshShaderFromSpv: graphics not initialized");
1096 if (fragSpv.empty())
throw Exception(
"newMeshShaderFromSpv: empty fragment SPIR-V");
1097 if (!mesh3dShaderPipelineLayout)
1098 throw Exception(
"newMeshShaderFromSpv: mesh3d pipeline layout missing");
1100 std::vector<uint32_t>
vert = vertSpv;
1102 vert.assign(mesh3d_vert_spv, mesh3d_vert_spv + mesh3d_vert_spv_count);
1103 if (
vert[0] != 0x07230203 || fragSpv[0] != 0x07230203)
1104 throw Exception(
"newMeshShaderFromSpv: SPIR-V magic mismatch");
1106 auto gpu = std::make_unique<GpuShader>();
1107 gpu->isMesh3D =
true;
1108 gpu->pipelineLayout = mesh3dShaderPipelineLayout;
1109 gpu->mesh3dPipeline = createMesh3DStylePipeline(
vert, fragSpv, mesh3dShaderPipelineLayout,
1110 activeScenePass(), activeSceneSamples());
1113 gpu->mesh3dXrayPipeline = createMesh3DXrayPipeline(
vert, fragSpv, mesh3dShaderPipelineLayout,
1114 activeScenePass(), activeSceneSamples());
1116 auto sh = std::make_unique<Shader>();
1118 sh->setSpirv(std::move(
vert), fragSpv);
1119 sh->gpuHandle = gpu.get();
1120 gpu->owner = sh.get();
1123 ownedShaders.push_back(std::move(sh));
1124 ownedGpuShaders.push_back(std::move(gpu));
1129 const std::vector<uint32_t> &fragSpv) {
1130 ASSERT(initialized);
1131 if (!initialized)
throw Exception(
"newHairShaderFromSpv: graphics not initialized");
1132 if (fragSpv.empty())
throw Exception(
"newHairShaderFromSpv: empty fragment SPIR-V");
1133 if (!mesh3dShaderPipelineLayout)
1134 throw Exception(
"newHairShaderFromSpv: mesh3d pipeline layout missing");
1136 std::vector<uint32_t>
vert = vertSpv;
1138 vert.assign(mesh3d_hair_vert_spv, mesh3d_hair_vert_spv + mesh3d_hair_vert_spv_count);
1139 if (
vert[0] != 0x07230203 || fragSpv[0] != 0x07230203)
1140 throw Exception(
"newHairShaderFromSpv: SPIR-V magic mismatch");
1142 auto gpu = std::make_unique<GpuShader>();
1143 gpu->isMesh3D =
true;
1144 gpu->isHair3D =
true;
1145 gpu->pipelineLayout = mesh3dShaderPipelineLayout;
1146 gpu->mesh3dPipeline = createMesh3DHairPipeline(
vert, fragSpv, mesh3dShaderPipelineLayout,
1147 activeScenePass(), activeSceneSamples());
1149 auto sh = std::make_unique<Shader>();
1151 sh->setSpirv(std::move(
vert), fragSpv);
1152 sh->gpuHandle = gpu.get();
1153 gpu->owner = sh.get();
1156 ownedShaders.push_back(std::move(sh));
1157 ownedGpuShaders.push_back(std::move(gpu));
1165 auto gpuIt = std::find_if(ownedGpuShaders.begin(), ownedGpuShaders.end(),
1166 [&](
const std::unique_ptr<GpuShader> &g) {
1167 return g.get() == gpu;
1169 if (gpuIt == ownedGpuShaders.end())
return false;
1171 auto shIt = std::find_if(ownedShaders.begin(), ownedShaders.end(),
1172 [&](
const std::unique_ptr<Shader> &
s) {
1173 return s.get() == shader;
1175 if (shIt == ownedShaders.end())
return false;
1179 waitForSharedGpuResources();
1180 if (gpu->swapchainPipeline)
device->destroyPipeline(gpu->swapchainPipeline);
1181 if (gpu->offscreenPipeline)
device->destroyPipeline(gpu->offscreenPipeline);
1182 if (gpu->mesh3dPipeline)
device->destroyPipeline(gpu->mesh3dPipeline);
1183 if (gpu->mesh3dXrayPipeline)
device->destroyPipeline(gpu->mesh3dXrayPipeline);
1184 shader->gpuHandle =
nullptr;
1185 ownedGpuShaders.erase(gpuIt);
1187 (void)shIt->release();
1188 ownedShaders.erase(shIt);
1193 if (fragGlsl.empty())
throw Exception(
"newMeshShader: empty fragment GLSL");
1194 std::vector<uint32_t>
vert;
1195 if (!vertGlsl.empty())
vert = compileGlslWithGlslc(vertGlsl,
"vert");
1196 auto frag = compileGlslWithGlslc(fragGlsl,
"frag");
1201 throw Exception(
"newMeshShaderFromWgsl: WGSL mesh shaders are only supported on the "
1202 "WebGPU backend; use newMeshShaderFromSpv on Vulkan.");
1205void Graphics::flushBatch() {
1206 if (!initialized)
return;
1209 if (!oc)
throw Exception(
"flushBatch: active canvas is not an OffscreenCanvas");
1210 flushToOffscreen(oc);
1216void Graphics::flushToOffscreen(OffscreenCanvas *canvas) {
1217 auto solid = std::move(solidBatches);
1218 auto textured = std::move(texturedBatches);
1219 auto lit = std::move(litBatches);
1220 auto spans = std::move(overlaySpans);
1223 const Color cc = canvas->pendingClearColor();
1224 const bool needClear = canvas->takePendingClear();
1225 bool hasSolid =
false;
1226 for (
const auto &sb : solid)
1227 if (!sb.batch.empty()) hasSolid = true;
1228 if (!hasSolid && textured.empty() && lit.empty() && !needClear)
return;
1233 waitForSharedGpuResources();
1235 vkb::executeImmediately(
device.instance, uploadPool,
device.getQueue(vkb::QueueType::graphics),
1236 [&](vk::CommandBuffer cb) {
1237 canvas->colorImage().setLayout(cb, vk::ImageLayout::eColorAttachmentOptimal);
1240 vk::ClearColorValue(std::array<float, 4>{cc.r, cc.g, cc.b, cc.a})};
1241 vk::RenderPassBeginInfo rpBegin{};
1242 rpBegin.renderPass = offscreenRenderPass;
1243 rpBegin.framebuffer = canvas->framebuffer();
1244 rpBegin.renderArea.extent =
1245 vk::Extent2D{uint32_t(canvas->getWidth()), uint32_t(canvas->getHeight())};
1246 rpBegin.clearValueCount = 1;
1247 rpBegin.pClearValues = &cv;
1248 canvas->colorImage().beginColorAttachment();
1249 cb.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
1251 setViewportAndScissor(cb, uint32_t(canvas->getWidth()),
1252 uint32_t(canvas->getHeight()));
1254 std::vector<vkb::HostVertexBuffer> &solidBufs =
1255 offscreenBuffers.solidBufs;
1256 std::vector<vkb::HostVertexBuffer> &texBufs = offscreenBuffers.texBufs;
1257 size_t texBufIndex = 0;
1258 const int vw = canvas->getWidth();
1259 const int vh = canvas->getHeight();
1261 auto offscreenTexPipe = [&](
BlendMode mode) -> vk::Pipeline {
1264 return offscreenAdditiveTexPipeline;
1266 return offscreenOpaqueTexPipeline;
1269 return offscreenTexPipeline;
1272 auto offscreenSolidPipe = [&](
BlendMode mode) -> vk::Pipeline {
1275 return offscreenAdditiveSolidPipeline;
1277 return offscreenSolidAlphaPipeline;
1280 return offscreenSolidPipeline;
1284 auto drawOffscreenTextured = [&](TexturedBatch &
tb) {
1285 if (
tb.batch.empty() || !
tb.texture || !
tb.texture->gpuHandle)
return;
1286 auto *gpu =
static_cast<GpuTexture *
>(
tb.texture->gpuHandle);
1287 vk::DescriptorSet texSet = gpu->descriptorSet;
1288 if (
tb.depth &&
tb.depth->gpuHandle) {
1289 auto *depthGpu =
static_cast<GpuTexture *
>(
tb.depth->gpuHandle);
1290 if (vk::DescriptorSet combo = post2SetFor(gpu, depthGpu))
1293 Batcher ndc =
tb.batch;
1295 std::vector<TexturedVertex> gpuVerts;
1296 gpuVerts.reserve(ndc.vertices().size());
1297 for (
const auto &
v : ndc.vertices())
1298 gpuVerts.push_back(TexturedVertex{
v.
pos,
v.
color,
v.uv});
1300 if (texBufIndex >= texBufs.size()) texBufs.emplace_back();
1301 vkb::HostVertexBuffer &vb = texBufs[texBufIndex++];
1302 vb.allocate<TexturedVertex>(frameToken(),
device, gpuVerts);
1304 if (
tb.shader &&
tb.shader->gpuHandle) {
1305 ensureShaderOffscreenPipeline(
tb.shader);
1306 auto *gs =
static_cast<GpuShader *
>(
tb.shader->gpuHandle);
1307 if (!gs->offscreenPipeline)
return;
1308 cb.bindPipeline(vk::PipelineBindPoint::eGraphics,
1309 gs->offscreenPipeline);
1310 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
1311 shaderPipelineLayout, 0, 1,
1312 &texSet, 0,
nullptr);
1313 cb.pushConstants(shaderPipelineLayout,
1314 vk::ShaderStageFlagBits::eVertex |
1315 vk::ShaderStageFlagBits::eFragment,
1317 tb.shader->pushConstantData());
1319 vk::Pipeline pipe = offscreenTexPipe(
tb.blend);
1321 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, pipe);
1322 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
1323 texPipelineLayout, 0, 1,
1324 &texSet, 0,
nullptr);
1326 vk::DeviceSize offset = 0;
1327 cb.bindVertexBuffers(0, 1, vb, &offset);
1328 cb.draw(uint32_t(gpuVerts.size()), 1, 0, 0);
1331 std::vector<bool> solidUploaded(solid.size(),
false);
1332 auto uploadSolid = [&](
size_t idx) {
1333 if (
idx >= solid.size() || solidUploaded[
idx] ||
1334 solid[
idx].batch.empty())
1336 vk::Pipeline pipe = offscreenSolidPipe(solid[
idx].blend);
1338 Batcher ndc = solid[
idx].batch;
1340 std::vector<ColorVertex> gpuVerts;
1341 gpuVerts.reserve(ndc.vertices().size());
1342 for (
const auto &
v : ndc.vertices())
1343 gpuVerts.push_back(ColorVertex{
v.
pos,
v.
color});
1344 if (solidBufs.size() <=
idx) solidBufs.resize(
idx + 1);
1345 solidBufs[
idx].allocate<ColorVertex>(frameToken(),
device,
1347 solidUploaded[
idx] =
true;
1350 auto drawSolidSpan = [&](uint32_t batchIndex, uint32_t begin,
1352 if (batchIndex >= solid.size() || count == 0 ||
1353 solid[batchIndex].batch.empty())
1355 vk::Pipeline pipe = offscreenSolidPipe(solid[batchIndex].blend);
1357 uploadSolid(batchIndex);
1358 vk::DeviceSize offset = 0;
1359 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, pipe);
1360 cb.bindVertexBuffers(0, 1, solidBufs[batchIndex], &offset);
1361 cb.draw(count, 1, begin, 0);
1364 if (!spans.empty()) {
1365 for (
const auto &sp : spans) {
1366 if (sp.kind == OverlayKind::Solid)
1367 drawSolidSpan(sp.index, sp.vertBegin, sp.vertCount);
1368 else if (sp.kind == OverlayKind::Textured &&
1369 sp.index < textured.size())
1370 drawOffscreenTextured(textured[sp.index]);
1371 else if (sp.kind == OverlayKind::Lit && offscreenLitPipeline &&
1372 sp.index < lit.size()) {
1373 std::vector<LitBatch> one;
1374 one.push_back(std::move(lit[sp.index]));
1375 drawLitBatches(cb, vw, vh, offscreenLitPipeline, one,
1376 texBufs, texBufIndex,
true);
1380 for (
size_t i = 0; i < solid.size(); ++i) {
1381 if (!solid[i].batch.empty())
1382 drawSolidSpan(uint32_t(i), 0,
1383 uint32_t(solid[i].batch.vertices().size()));
1385 for (
auto &
tb : textured) drawOffscreenTextured(
tb);
1386 if (offscreenLitPipeline)
1387 drawLitBatches(cb, vw, vh, offscreenLitPipeline, lit, texBufs,
1392 canvas->colorImage().endSampledLayout();
1396void Graphics::abortOpen3DFrame() {
1397 const bool hadScene = sceneColorPassOpen;
1398 const bool had3D = swapchainPassOpen;
1400 if (hadScene) endSceneColorRenderPass();
1402 sceneColorPassOpen =
false;
1408 if (hadScene) beginSwapchainColorPass();
1409 presentRecording = swapchainPass.endRenderPass();
1412 ? recordSwapchainReadback(presentRecording.commandBuffer())
1414 presentRecording.end().submitAndPresent();
1415 presentRecording = {};
1417 hasPresentedFrame =
true;
1418 readbackReady =
true;
1419 readbackCpuSynced =
false;
1424 presentRecording = {};
1426 swapchainPassOpen =
false;
1427 sceneColorPassOpen =
false;
1429 hasPendingClear =
false;
1430 flushingSwapchain_ =
false;
1434void Graphics::flushToSwapchain() {
1435 if (flushingSwapchain_)
return;
1436 flushingSwapchain_ =
true;
1437 bool completed =
false;
1442 g->flushingSwapchain_ =
false;
1443 if (!*completed) g->abortOpen3DFrame();
1445 } guard{
this, &completed};
1447 const bool continue3D = swapchainPassOpen;
1448 const bool hadScenePass = sceneColorPassOpen;
1451 endSceneColorRenderPass();
1452 queueSceneColorResolve();
1459 if (!beginPresentCommandBuffer()) {
1460 dropPendingOffscreenPasses();
1461 hasPendingClear =
false;
1465 recordDeferredFrameGraph();
1472 renderUiOverlayPass();
1475 if (hadScenePass || !continue3D) {
1476 beginSwapchainColorPass();
1477 swapchainPassOpen =
true;
1480 auto solid = std::move(solidBatches);
1481 auto textured = std::move(texturedBatches);
1482 auto lit = std::move(litBatches);
1483 auto spans = std::move(overlaySpans);
1484 auto engineSpans = std::move(engine3DSpans);
1485 auto sceneResolve = std::move(pendingSceneResolve);
1486 auto uiResolve = std::move(pendingUiResolve);
1487 const bool autoScene = hadScenePass && !sceneColorComposited;
1491 auto &cb = currentPresentCb();
1492 setViewportAndScissor(cb, swapchain.extent.width, swapchain.extent.height);
1496 auto &frameBufs = currentFrame2DBuffers();
1497 std::vector<vkb::HostVertexBuffer> &solidBufs = frameBufs.solidBufs;
1498 std::vector<vkb::HostVertexBuffer> &texBufs = frameBufs.texBufs;
1499 size_t texBufIndex = 0;
1501 auto swapchainTexPipe = [&](
BlendMode mode) -> vk::Pipeline {
1504 return additiveTexPipeline;
1506 return opaqueTexPipeline;
1512 auto swapchainSolidPipe = [&](
BlendMode mode) -> vk::Pipeline {
1515 return additiveSolidPipeline;
1517 return solidAlphaPipeline;
1524 auto drawTextured = [&](TexturedBatch &
tb) {
1525 if (
tb.batch.empty() || !
tb.texture || !
tb.texture->gpuHandle)
return;
1526 auto *gpu =
static_cast<GpuTexture *
>(
tb.texture->gpuHandle);
1527 vk::DescriptorSet texSet = gpu->descriptorSet;
1528 if (
tb.depth &&
tb.depth->gpuHandle) {
1529 auto *depthGpu =
static_cast<GpuTexture *
>(
tb.depth->gpuHandle);
1530 if (vk::DescriptorSet combo = post2SetFor(gpu, depthGpu)) texSet = combo;
1532 Batcher ndc =
tb.batch;
1534 std::vector<TexturedVertex> gpuVerts;
1535 gpuVerts.reserve(ndc.vertices().size());
1536 for (
const auto &
v : ndc.vertices())
1537 gpuVerts.push_back(TexturedVertex{
v.
pos,
v.
color,
v.uv});
1539 if (texBufIndex >= texBufs.size()) texBufs.emplace_back();
1540 vkb::HostVertexBuffer &vb = texBufs[texBufIndex++];
1541 vb.allocate<TexturedVertex>(frameToken(),
device, gpuVerts);
1543 if (
tb.shader &&
tb.shader->gpuHandle) {
1544 auto *gs =
static_cast<GpuShader *
>(
tb.shader->gpuHandle);
1545 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, gs->swapchainPipeline);
1546 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, shaderPipelineLayout, 0, 1,
1547 &texSet, 0,
nullptr);
1548 cb.pushConstants(shaderPipelineLayout,
1549 vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, 0,
1552 vk::Pipeline pipe = swapchainTexPipe(
tb.blend);
1554 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, pipe);
1555 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, texPipelineLayout, 0, 1,
1556 &texSet, 0,
nullptr);
1558 vk::DeviceSize offset = 0;
1559 cb.bindVertexBuffers(0, 1, vb, &offset);
1560 cb.draw(uint32_t(gpuVerts.size()), 1, 0, 0);
1563 std::vector<bool> solidUploaded(solid.size(),
false);
1564 auto uploadSolid = [&](
size_t idx) {
1565 if (
idx >= solid.size() || solidUploaded[
idx] || solid[
idx].batch.empty())
return;
1566 vk::Pipeline pipe = swapchainSolidPipe(solid[
idx].blend);
1568 Batcher ndc = solid[
idx].batch;
1570 std::vector<ColorVertex> gpuVerts;
1571 gpuVerts.reserve(ndc.vertices().size());
1572 for (
const auto &
v : ndc.vertices())
1573 gpuVerts.push_back(ColorVertex{
v.
pos,
v.
color});
1574 if (solidBufs.size() <=
idx) solidBufs.resize(
idx + 1);
1575 solidBufs[
idx].allocate<ColorVertex>(frameToken(),
device, gpuVerts);
1576 solidUploaded[
idx] =
true;
1579 auto drawSolidSpan = [&](uint32_t batchIndex, uint32_t begin, uint32_t count) {
1580 if (batchIndex >= solid.size() || count == 0 || solid[batchIndex].batch.empty())
return;
1581 vk::Pipeline pipe = swapchainSolidPipe(solid[batchIndex].blend);
1583 uploadSolid(batchIndex);
1584 vk::DeviceSize offset = 0;
1585 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, pipe);
1586 cb.bindVertexBuffers(0, 1, solidBufs[batchIndex], &offset);
1587 cb.draw(count, 1, begin, 0);
1590 auto replaySpans = [&](
const std::vector<OverlaySpan> &
list) {
1591 for (
const auto &sp :
list) {
1592 if (sp.kind == OverlayKind::Solid && sp.index < solid.size() && sp.vertCount > 0) {
1593 drawSolidSpan(sp.index, sp.vertBegin, sp.vertCount);
1594 }
else if (sp.kind == OverlayKind::Textured && texPipeline &&
1595 sp.index < textured.size()) {
1596 drawTextured(textured[sp.index]);
1597 }
else if (sp.kind == OverlayKind::Lit && lit2dPipeline && sp.index < lit.size()) {
1598 std::vector<LitBatch> one;
1599 one.push_back(std::move(lit[sp.index]));
1600 drawLitBatches(cb,
width,
height, lit2dPipeline, one, texBufs, texBufIndex,
false);
1605 bool engineDrawn =
false;
1606 auto drawEngine3D = [&]() {
1607 if (engineDrawn)
return;
1609 replaySpans(engineSpans);
1614 if (autoScene && sceneResolve) {
1615 drawTextured(*sceneResolve);
1619 if (spans.empty()) {
1620 for (
size_t i = 0; i < solid.size(); ++i) {
1621 if (!solid[i].batch.empty())
1622 drawSolidSpan(uint32_t(i), 0, uint32_t(solid[i].batch.vertices().size()));
1625 for (
auto &
tb : textured) drawTextured(
tb);
1627 if (lit2dPipeline) drawLitBatches(cb,
width,
height, lit2dPipeline, lit, texBufs, texBufIndex,
1630 for (
const auto &sp : spans) {
1631 if (sp.kind == OverlayKind::Solid && sp.index < solid.size() && sp.vertCount > 0) {
1632 drawSolidSpan(sp.index, sp.vertBegin, sp.vertCount);
1633 }
else if (sp.kind == OverlayKind::Textured && texPipeline &&
1634 sp.index < textured.size()) {
1635 drawTextured(textured[sp.index]);
1636 if (textured[sp.index].texture == sceneTex) drawEngine3D();
1637 }
else if (sp.kind == OverlayKind::Lit && lit2dPipeline && sp.index < lit.size()) {
1638 std::vector<LitBatch> one;
1639 one.push_back(std::move(lit[sp.index]));
1640 drawLitBatches(cb,
width,
height, lit2dPipeline, one, texBufs, texBufIndex,
false);
1645 if (uiResolve) drawTextured(*uiResolve);
1653 VkCommandBuffer raw =
static_cast<VkCommandBuffer
>(cb);
1657 presentRecording = swapchainPass.endRenderPass();
1659 const bool captured =
1661 presentRecording.end().submitAndPresent();
1662 presentRecording = {};
1664 hasPresentedFrame =
true;
1665 readbackReady =
true;
1666 readbackCpuSynced =
false;
1668 hasPendingClear =
false;
1669 swapchainPassOpen =
false;
image::ImageData::Colorf color
Accumulates solid / textured quads in logical (Y-down) coordinates. Used by RenderSystem; not a publi...
virtual void clear(std::optional< Color > color, std::optional< int > stencil, std::optional< double > depth)=0
virtual Texture * newCubemap(int faceSize, const uint8_t *rgbaFaces)=0
Create an RGBA8 cubemap from 6 faces packed as +X,-X,+Y,-Y,+Z,-Z (each faceSize×faceSize,...
bool screenReadbackEnabled
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....
virtual bool releaseTexture(Texture *texture)
Eagerly releases a texture created by this Graphics.
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
virtual image::ImageData * readGBufferToImageData(const std::string &name)
Read a G-buffer attachment back to CPU as RGBA8. name is one of "depth" (RGBA8 linear depth),...
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 Canvas * getCanvas() const =0
virtual Shader * newShader(const std::string &vertGlsl, const std::string &fragGlsl)=0
Compile GLSL source with glslc (must be on PATH). Empty vertGlsl → default textured vert....
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
virtual bool reloadTextureFromFile(const std::string &filename)=0
Reload a path-cached texture from disk in place (pointer stable). False if unbound.
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
virtual float getMaxAnisotropy() const =0
Device max supported anisotropy (1 if unsupported). Valid after initWithWindow.
virtual Texture * newTextureFromFile(const std::string &filename)=0
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 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 void setTextureSampler(Texture *texture, const TextureSampler &sampler)=0
Recreate the sampler for an existing texture (keeps image / mip chain). No-op when texture is null or...
virtual void drawTexturedRect(Texture *texture, float x, float y, float w, float h, const Color &color)=0
virtual Texture * getSceneColorTexture()
Sampleable 3D color target for the current frame (RGB = lit, A = linear depth). Valid after begin3DFr...
virtual void drawSolidRect(float x, float y, float w, float h, const Color &color, BlendMode blend=BlendMode::Alpha)=0
Internal immediate-mode helper used by RenderSystem / Batcher.
PresentOverlayFn presentOverlayFn_
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)=0
Lit 2D draw (albedo + normal map). Uses Lighting2DUBO from setLighting2D. normal may be null → treate...
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,...
Declarative, compilable 3D render control.
static constexpr uint32_t kPushConstantBytes
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
image::ImageData * renderEntityIdMask(const std::vector< eve::graphics::Graphics::EntityIdDraw > &draws, const glm::mat4 &viewProj, int width, int height) override
Color getPixel(int x, int y) override
image::ImageData * newImageData() override
void clear(std::optional< Color > color, std::optional< int > stencil, std::optional< double > depth) override
Represents raw pixel data.
ref is a smart pointer that automatically calls ref() and unref() on the object. Type T must be a sub...
FilterMode
Mag/min filter for texture sampling.
MipmapMode
Mipmap filter; Disabled turns off mip sampling (maxLod clamped to 0).
int mipmapCountForSize(int width, int height)
Full mip chain count for a 2D image (including base level).
glm::vec4 Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
BlendMode
2D quad blend mode (drawn in draw order within a layer).
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...
Options for Graphics::newTexture / newCubemap. When generateMipmaps is true and sampler....
static TextureCreateInfo withMipmaps(bool aniso=true, float maxAniso=16.f)
Sampler state for a Texture (filter, wrap, mip LOD, anisotropy). Defaults match historical engine beh...
float maxAnisotropy
1 = off; values >1 enable anisotropic filtering when the device supports it.
float maxLod
Inclusive upper LOD clamp. Large values mean "use all available mips".