载入中...
搜索中...
未找到
Graphics2D.cpp
浏览该文件的文档.
1// Vulkan backend implementation — 2D drawing, textures and batching.
2//
3// Re-split from the merged dev single-TU Graphics.cpp (pure move;
4// dev changes preserved). Shared helpers live in GraphicsInternal.h.
5
7#include "graphics/Light.h"
14
15#include <SDL2/SDL.h>
16#include <SDL2/SDL_vulkan.h>
17
18#include <algorithm>
19#include <array>
20#include <cmath>
21#include <cstdio>
22#include <cstdlib>
23#include <cstdint>
24#include <cstring>
25#include <functional>
26#include <stdexcept>
27#include <string>
28#include <vector>
29
30#include "common/Exception.h"
32#include "common/config.h"
34#include "image/Image.h"
35#include "image/ImageData.h"
36#include "zeroerr/assert.h"
37
38#include <memory>
39
40
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>
46
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"
54
55namespace eve::graphics::vulkan {
56
57namespace {
58
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;
77 } else {
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;
84 }
85 cb.pipelineBarrier(srcStages, dstStages, {}, 0, nullptr, 0, nullptr, 1, &barrier);
86 image.setCurrentLayout(newLayout);
87}
88
89} // namespace
90
91// Shader helper functions. They used to live in GraphicsInternal.h (which is
92// an anonymous-namespace header copied into every backend TU); only this file
93// uses them, so they are defined here to avoid the per-TU duplication and the
94// header's dependency on the filesystem module.
95namespace {
96
97std::vector<uint32_t> loadSpirvBytes(const void *data, size_t size) {
98 if (!data || size < 4 || (size % 4) != 0)
99 throw Exception("SPIR-V: invalid size %zu", size);
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);
104}
105
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());
111}
112
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);
119 try {
120 return compileGlslToSpirv(source, glslStage, std::string("eve_shader.") + stage);
121 } catch (const std::exception &error) {
122 // The shared helper throws plain runtime errors (it also runs on CPU
123 // worker threads); wrap them into the engine exception callers expect.
124 throw Exception("newShader: GLSL compile failed for %s:\n%s", stage, error.what());
125 }
126}
127
128} // namespace
129
130void Graphics::ensurePresentCaptureHook() {
131 // Screen readback is recorded inline into the present command buffer (see
132 // flushToSwapchain / abortOpen3DFrame), so the present model must never
133 // get a post-submit hook: drawFrame() waits for this frame's fence
134 // whenever the hook is set, which serializes every frame and erases the
135 // multi-frame overlap we rely on for async rendering.
136 presentModel.after_render_before_present = nullptr;
137}
138
139void Graphics::ensureReadbackSlots() {
140 if (pixelWidth <= 0 || pixelHeight <= 0) return;
141 const size_t bytes = size_t(pixelWidth) * size_t(pixelHeight) * 4;
142 const size_t want = std::max<size_t>(2, frameSlotCount());
143 if (!screenReadbackSlots.empty() && screenReadbackSlots.size() >= want &&
144 screenReadbackBytes == bytes)
145 return;
146 // Recreating the staging ring must never race in-flight copies. Callers
147 // run on the render thread, and a size change implies the swapchain was
148 // rebuilt under waitIdle (rebuildSwapchainIfNeeded / recreate path).
149 for (auto &slot : screenReadbackSlots) {
150 if (slot.mapped) {
151 slot.staging.unmap();
152 slot.mapped = nullptr;
153 }
154 slot.staging.release();
155 }
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);
163 }
164 screenReadbackBytes = bytes;
165 readbackReady = false;
166 readbackCpuSynced = false;
167 readbackWriteSlot = 0;
168}
169
170bool Graphics::recordSwapchainReadback(vk::CommandBuffer cb) {
171 if (!screenReadbackEnabled || pixelWidth <= 0 || pixelHeight <= 0) return false;
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;
177 readbackBgra = bgra;
178
179 const uint32_t imageIndex = presentModel.acquired_image_index;
180 auto &images = swapchain.get_images();
181 if (imageIndex >= images.size()) return false;
182
183 ensureReadbackSlots();
184 if (screenReadbackSlots.empty()) return false;
185
186 const size_t slot = size_t(presentRecording.slot().index) % screenReadbackSlots.size();
187 readbackWriteSlot = slot;
188 const vk::Image image = images[imageIndex];
189 // Recorded at the end of the present command buffer: the swapchain render
190 // pass has already transitioned the image back to PresentSrcKHR, and the
191 // copy runs in the same submit+present — no extra queue submit, no
192 // waitIdle, no per-frame buffer allocation.
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};
200 // The render pass has already transitioned the image to PresentSrcKHR, but
201 // there is no fence between it and this copy — the barrier must order the
202 // render-pass color writes against the transfer explicitly.
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,
207 &toTransfer);
208
209 vk::BufferImageCopy region{};
210 region.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
211 region.imageExtent = vk::Extent3D{uint32_t(pixelWidth), uint32_t(pixelHeight), 1};
212 cb.copyImageToBuffer(image, vk::ImageLayout::eTransferSrcOptimal,
213 screenReadbackSlots[slot].staging.buffer, region);
214
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,
222 &toPresent);
223 return true;
224}
225
226void Graphics::syncReadbackCpu() {
227 if (!readbackReady || screenReadbackSlots.empty()) return;
228 if (readbackCpuSynced && !lastFrameRgba.empty()) return;
229 if (readbackWriteSlot >= screenReadbackSlots.size()) return;
230
231 const size_t bytes = screenReadbackBytes;
232 if (bytes == 0) return;
233
234 // The newest copy lives in the present submission of this frame slot; wait
235 // only that slot's fence instead of a device-wide waitIdle.
236 presentModel.waitForFrameSlot(readbackWriteSlot);
237
238 auto &slot = screenReadbackSlots[readbackWriteSlot];
239 if (!slot.mapped)
240 slot.mapped = slot.staging.map();
241
242 lastFrameRgba.resize(bytes);
243 if (readbackBgra) {
244 // BGRA -> RGBA byte swap, two pixels per 64-bit word (masked swap).
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);
253 }
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);
259 }
260 } else {
261 std::memcpy(lastFrameRgba.data(), slot.mapped, bytes);
262 }
263 readbackCpuSynced = true;
264}
265
266void Graphics::destroyReadbackResources() {
267 for (auto &slot : screenReadbackSlots) {
268 if (slot.mapped) {
269 slot.staging.unmap();
270 slot.mapped = nullptr;
271 }
272 slot.staging.release();
273 }
274 screenReadbackSlots.clear();
275 screenReadbackBytes = 0;
276 readbackReady = false;
277 readbackCpuSynced = false;
278 hasPresentedFrame = false;
279}
280
282 syncReadbackCpu();
283 if (!hasPresentedFrame || lastFrameRgba.empty())
284 throw Exception("Graphics::newImageData: no presented frame");
285 auto *img = new image::ImageData(pixelWidth, pixelHeight, "RGBA8");
286 std::memcpy(img->getData(), lastFrameRgba.data(), lastFrameRgba.size());
287 return img;
288}
289
291 syncReadbackCpu();
292 if (!hasPresentedFrame || lastFrameRgba.empty())
293 throw Exception("Graphics::getPixel: no presented frame");
294 if (x < 0 || y < 0 || x >= width || y >= height)
295 throw Exception("Graphics::getPixel: out of bounds (%d,%d)", x, y);
296
297 const int pxX = (width > 0) ? int((int64_t(x) * pixelWidth) / width) : x;
298 const int pxY = (height > 0) ? int((int64_t(y) * pixelHeight) / height) : y;
299 const int cx = std::min(std::max(pxX, 0), pixelWidth - 1);
300 const int cy = std::min(std::max(pxY, 0), pixelHeight - 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;
306 // If surface ended up sRGB, convert encoded bytes back to linear Color space.
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);
311 };
312 r = toLinear(r);
313 g = toLinear(g);
314 b = toLinear(b);
315 }
316 return Color(r, g, b, a);
317}
318
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;
323 // G-buffer pipeline / render pass are created lazily by createGBufferResources,
324 // so that must run before the availability check.
325 createGBufferResources(maskW, maskH);
326 if (!gbufferPipeline || !gbufferRenderPass) return nullptr;
327 auto *slot = currentGBufferSlot();
328 if (!slot || !slot->framebuffer || !whiteTexture) return nullptr;
329
330 // Render each mesh with a flat idColor into the G-buffer albedo attachment
331 // (location 2) by reusing the G-buffer pipeline (its fragment shader writes
332 // outAlbedo = albedo * tint, so passing a white texture + idColor tint gives
333 // a per-pixel flat entity-ID color).
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));
338 };
339 for (const auto &d : draws) {
340 if (!d.mesh || !d.mesh->gpuHandle) continue;
341 GBufferDraw gd{};
342 gd.mesh = d.mesh;
343 gd.albedo = whiteTexture;
344 gd.push.mvp = viewProj * d.model;
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);
352 }
353 if (idDraws.empty()) return nullptr;
354
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);
361
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) {
386 auto *gpuTex = static_cast<GpuTexture *>(whiteTexture->gpuHandle);
387 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
388 gbufferPipelineLayout, 0, 1,
389 gpuTex->descriptorSet.ptr(), 0, nullptr);
390 }
391 cb.pushConstants(gbufferPipelineLayout,
392 vk::ShaderStageFlagBits::eVertex |
393 vk::ShaderStageFlagBits::eFragment,
394 0, sizeof(GBufferPush), &d.push);
395 drawIndexedMesh(cb, *gpuMesh);
396 }
397 cb.endRenderPass();
398 slot->normal.endSampledLayout();
399 slot->depthColor.endSampledLayout();
400 slot->albedo.endSampledLayout();
401 slot->depth.endSampledLayout();
402
403 // Copy the albedo attachment (location 2 = ID colors) to CPU.
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,
410 region);
411 slot->albedo.setLayout(cb, vk::ImageLayout::eShaderReadOnlyOptimal);
412 });
413
414 auto *img = new image::ImageData(int(w), int(h), "RGBA8");
415 void *mapped = staging.map();
416 std::memcpy(img->getData(), mapped, size_t(byteSize));
417 staging.unmap();
418 staging.release();
419
420 // 让 RenderControl 的 GBuffer 也指向该槽位(镜像 endGBufferPass),这样
421 // 上层可通过 getDepthTexture()/getNormalTexture() 读取本次离屏 ID 渲染
422 // 生成的深度/法线(供 capture_render_frame 的 depth/normal 复用)。
423 if (RenderControl *rc = getRenderControl()) {
424 rc->getGBuffer()->setTargets(int(w), int(h), &slot->depthColorTex, &slot->normalTex, &slot->albedoTex,
425 &slot->depthTex, &slot->pbrParamsTex, &slot->emissiveTex);
426 }
427 return img;
428}
429
430image::ImageData *Graphics::readGBufferToImageData(const std::string &attachment) {
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; // RGBA8 linear depth
437 else if (attachment == "normal")
438 src = &slot->normal;
439 else if (attachment == "albedo")
440 src = &slot->albedo;
441 else if (attachment == "pbrParams")
442 src = &slot->pbrParams;
443 else if (attachment == "emissive")
444 src = &slot->emissive;
445 else
446 return nullptr;
447
448 const uint32_t w = uint32_t(gbufferWidth);
449 const uint32_t h = uint32_t(gbufferHeight);
450 if (w == 0 || h == 0) return nullptr;
451
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);
468 });
469
470 auto *img = new image::ImageData(int(w), int(h), "RGBA8");
471 void *mapped = staging.map();
472 std::memcpy(img->getData(), mapped, size_t(byteSize));
473 staging.unmap();
474 staging.release();
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) {
479 pixels[i * 4u + 1u] = pixels[i * 4u];
480 pixels[i * 4u + 2u] = pixels[i * 4u];
481 }
482 }
483 return img;
484}
485
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)
492 return nullptr;
493 vkb::ColorTarget *src = nullptr;
494 if (attachment == "normal")
495 src = &slot->normal;
496 else if (attachment == "params")
497 src = &slot->params;
498 else if (attachment == "albedo")
499 src = &slot->albedo;
500 else
501 return nullptr;
502
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();
507
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) {
514 // The G-buffer was submitted through the production FrameGraph
515 // above. Queue order makes it visible to this immediate pass.
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,
521 1};
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);
528 });
529
530 auto *img = new image::ImageData(int(w), int(h), "RGBA8");
531 void *mapped = staging.map();
532 std::memcpy(img->getData(), mapped, size_t(byteSize));
533 staging.unmap();
534 staging.release();
535 return img;
536}
537
539 ASSERT(initialized);
540 ASSERT_GT(w, 0);
541 ASSERT_GT(h, 0);
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);
546 Canvas *raw = c.get();
547 ownedCanvases.push_back(std::move(c));
548 return raw;
549}
550
552 ASSERT(initialized);
553 ASSERT_GT(w, 0);
554 ASSERT_GT(h, 0);
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);
559 Canvas *raw = c.get();
560 ownedCanvases.push_back(std::move(c));
561 return raw;
562}
563
565 Canvas *next = canvas;
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();
575 }
576 activeCanvas = next;
577}
578
580 return activeCanvas != nullptr;
581}
582
584 return activeCanvas ? activeCanvas : const_cast<Graphics *>(this);
585}
586
587void Graphics::setViewportSize(int newW, int newH, int newPw, int newPh) {
588 bool changed = (newW != width) || (newH != height) || (newPw != pixelWidth) || (newPh != pixelHeight);
589 width = newW;
590 height = newH;
591 pixelWidth = newPw;
592 pixelHeight = newPh;
593 if (initialized && changed) swapchainDirty = true;
594}
595
596void Graphics::clear2DBatches() {
597 solidBatches.clear();
598 texturedBatches.clear();
599 litBatches.clear();
600 overlaySpans.clear();
601 engine3DSpans.clear();
602 gpuParticleDraws_.clear();
603 pendingSceneResolve.reset();
604 pendingUiResolve.reset();
605 sceneColorComposited = false;
606}
607
608void Graphics::noteSolidOverlay(uint32_t idx) {
609 if (idx >= solidBatches.size()) return;
610 const uint32_t n = uint32_t(solidBatches[idx].batch.vertices().size());
611 auto &spans = recordingEngine3D_ ? engine3DSpans : overlaySpans;
612 if (!spans.empty() && spans.back().kind == OverlayKind::Solid &&
613 spans.back().index == idx) {
614 spans.back().vertCount = n - spans.back().vertBegin;
615 return;
616 }
617 const uint32_t begin = n >= 6u ? n - 6u : 0u;
618 spans.push_back({OverlayKind::Solid, idx, begin, n - begin});
619}
620
621void Graphics::noteTexturedOverlay(Texture *tex, uint32_t idx) {
622 // Sampling scene color into an offscreen post-process target is not a
623 // swapchain composite. Marking it as one suppresses the real final
624 // resolve and leaves the presented frame at its clear color.
625 if (!recordingEngine3D_ && !activeCanvas && tex && tex == getSceneColorTexture())
626 sceneColorComposited = true;
627 auto &spans = recordingEngine3D_ ? engine3DSpans : overlaySpans;
628 if (!spans.empty() && spans.back().kind == OverlayKind::Textured && spans.back().index == idx)
629 return;
630 spans.push_back({OverlayKind::Textured, idx, 0, 0});
631}
632
633void Graphics::noteLitOverlay(uint32_t idx) {
634 auto &spans = recordingEngine3D_ ? engine3DSpans : overlaySpans;
635 if (!spans.empty() && spans.back().kind == OverlayKind::Lit && spans.back().index == idx)
636 return;
637 spans.push_back({OverlayKind::Lit, idx, 0, 0});
638}
639
640void Graphics::clear(std::optional<Color> color, std::optional<int>, std::optional<double>) {
641 // Keep 3D framebuffer contents when composing 2D on top of an open 3D pass.
642 if (frameHad3D && activeCanvas == nullptr) return;
643 clearColor = color.value_or(backgroundColor);
644 hasPendingClear = true;
645 clear2DBatches();
646 if (auto *oc = dynamic_cast<OffscreenCanvas *>(activeCanvas)) {
647 oc->clear(clearColor, std::nullopt, std::nullopt);
648 }
649}
650
651void Graphics::drawSolidRect(float x, float y, float w, float h, const Color &color,
653 auto it = std::find_if(solidBatches.begin(), solidBatches.end(),
654 [&](const SolidBatch &sb) { return sb.blend == blend; });
655 if (it == solidBatches.end()) {
656 solidBatches.push_back(SolidBatch{blend, Batcher{}});
657 it = solidBatches.end() - 1;
658 }
659 it->batch.addRect(x, y, w, h, color);
660 noteSolidOverlay(uint32_t(it - solidBatches.begin()));
661}
662
664 const int targetWidth = activeCanvas ? activeCanvas->getWidth() : getWidth();
665 const int targetHeight = activeCanvas ? activeCanvas->getHeight() : getHeight();
666 const auto triangles = resolvePrimitiveStrokes2D(canvas, {targetWidth, targetHeight});
667 if (triangles.vertices.empty()) return;
668 auto logicalPoint = [targetWidth, targetHeight](const PrimitiveTriangleVertex &vertex) {
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));
672 };
673 auto &spans = recordingEngine3D_ ? engine3DSpans : overlaySpans;
674 for (const ResolvedPrimitiveBatch2D &resolvedBatch : triangles.batches2D) {
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;
680 }
681 for (std::size_t i = resolvedBatch.firstVertex; i < resolvedBatch.firstVertex + resolvedBatch.vertexCount;
682 i += 3) {
683 it->batch.addTriangle(logicalPoint(triangles.vertices[i]), logicalPoint(triangles.vertices[i + 1]),
684 logicalPoint(triangles.vertices[i + 2]), triangles.vertices[i].color,
685 triangles.vertices[i + 1].color, triangles.vertices[i + 2].color);
686 }
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});
691 }
692}
693
694void Graphics::drawSolidRectRotated(float cx, float cy, float w, float h, float degrees,
695 const Color &color, BlendMode blend) {
696 auto it = std::find_if(solidBatches.begin(), solidBatches.end(),
697 [&](const SolidBatch &sb) { return sb.blend == blend; });
698 if (it == solidBatches.end()) {
699 solidBatches.push_back(SolidBatch{blend, Batcher{}});
700 it = solidBatches.end() - 1;
701 }
702 it->batch.addRectRotated(cx, cy, w, h, degrees, color);
703 noteSolidOverlay(uint32_t(it - solidBatches.begin()));
704}
705
706
707float Graphics::getMaxAnisotropy() const { return maxSamplerAnisotropy; }
708
709vk::Sampler Graphics::createVkSampler(const TextureSampler &sampler, uint32_t mipLevels) const {
710 auto wrapMode = [](bool repeat) {
711 return repeat ? vk::SamplerAddressMode::eRepeat : vk::SamplerAddressMode::eClampToEdge;
712 };
713 auto toFilter = [](FilterMode m) {
714 return m == FilterMode::Nearest ? vk::Filter::eNearest : vk::Filter::eLinear;
715 };
716 auto toMip = [](MipmapMode m) {
717 return m == MipmapMode::Nearest ? vk::SamplerMipmapMode::eNearest
718 : vk::SamplerMipmapMode::eLinear;
719 };
720
721 const bool useMips = sampler.mipmap != MipmapMode::Disabled && mipLevels > 1;
722 float maxLod = useMips ? std::min(sampler.maxLod, float(mipLevels - 1)) : 0.f;
723 if (maxLod < sampler.minLod) maxLod = sampler.minLod;
724
725 float aniso = 1.f;
726 bool enableAniso = false;
727 if (sampler.maxAnisotropy > 1.f && maxSamplerAnisotropy > 1.f) {
728 enableAniso = true;
729 aniso = std::min(sampler.maxAnisotropy, maxSamplerAnisotropy);
730 }
731
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)
736 .addressModeU(wrapMode(sampler.repeatU))
737 .addressModeV(wrapMode(sampler.repeatV))
738 .addressModeW(wrapMode(sampler.repeatW))
739 .mipLodBias(sampler.lodBias)
740 .anisotropyEnable(enableAniso ? VK_TRUE : VK_FALSE)
741 .maxAnisotropy(aniso)
742 .minLod(useMips ? sampler.minLod : 0.f)
743 .maxLod(maxLod)
744 .build(device);
745}
746
747
748void Graphics::drawTexturedRect(Texture *texture, float x, float y, float w, float h, const Color &color) {
749 drawTexturedRectUV(texture, x, y, w, h, 0.f, 0.f, 1.f, 1.f, color);
750}
751
752void Graphics::drawTexturedRectShader(Texture *texture, Shader *shader, float x, float y, float w,
753 float h, const Color &color) {
754 drawTexturedRectShaderUV(texture, shader, x, y, w, h, 0.f, 0.f, 1.f, 1.f, color);
755}
756
757void Graphics::drawTexturedRectUV(Texture *texture, float x, float y, float w, float h, float u0,
758 float v0, float u1, float v1, const Color &color) {
759 drawTexturedRectShaderUV(texture, currentShader, x, y, w, h, u0, v0, u1, v1, color);
760}
761
762void Graphics::drawTexturedRectShaderUV(Texture *texture, Shader *shader, float x, float y, float w,
763 float h, float u0, float v0, float u1, float v1,
764 const Color &color, bool rotatedUV, BlendMode blend) {
765 if (!texture) {
766 drawSolidRect(x, y, w, h, color, blend);
767 return;
768 }
769 if (texturedBatches.empty() || texturedBatches.back().texture != texture ||
770 texturedBatches.back().depth != nullptr ||
771 texturedBatches.back().shader != shader ||
772 texturedBatches.back().blend != blend) {
773 texturedBatches.push_back(TexturedBatch{texture, nullptr, shader, blend, Batcher{}});
774 }
775 texturedBatches.back().batch.addTexturedRect(x, y, w, h, color, u0, v0, u1, v1, rotatedUV);
776 noteTexturedOverlay(texture, uint32_t(texturedBatches.size() - 1));
777}
778
780 float w, float h, float degrees, float u0, float v0,
781 float u1, float v1, const Color &color,
782 bool rotatedUV, BlendMode blend) {
783 if (!texture) {
784 drawSolidRect(cx - w * 0.5f, cy - h * 0.5f, w, h, color, blend);
785 return;
786 }
787 if (texturedBatches.empty() || texturedBatches.back().texture != texture ||
788 texturedBatches.back().depth != nullptr ||
789 texturedBatches.back().shader != shader ||
790 texturedBatches.back().blend != blend) {
791 texturedBatches.push_back(TexturedBatch{texture, nullptr, shader, blend, Batcher{}});
792 }
793 texturedBatches.back().batch.addTexturedRectRotated(cx, cy, w, h, degrees, color, u0, v0, u1, v1,
794 rotatedUV);
795 noteTexturedOverlay(texture, uint32_t(texturedBatches.size() - 1));
796}
797
799 float y, float w, float h, const Color &tint) {
800 if (!color) {
801 drawSolidRect(x, y, w, h, tint);
802 return;
803 }
804 if (!depth) {
806 return;
807 }
808 if (texturedBatches.empty() || texturedBatches.back().texture != color ||
809 texturedBatches.back().depth != depth || texturedBatches.back().shader != shader) {
810 texturedBatches.push_back(
811 TexturedBatch{color, depth, shader, BlendMode::Alpha, Batcher{}});
812 }
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));
815}
816
818 Shader *shader, float x, float y, float w,
819 float h, const Color &tint) {
820 if (!color) {
821 drawSolidRect(x, y, w, h, tint);
822 return;
823 }
824 if (texturedBatches.empty() || texturedBatches.back().texture != color ||
825 texturedBatches.back().depth != depth || texturedBatches.back().motion != motion ||
826 texturedBatches.back().shader != shader) {
827 TexturedBatch batch{color, depth, shader, BlendMode::Opaque, Batcher{}};
828 batch.motion = motion;
829 texturedBatches.push_back(std::move(batch));
830 }
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));
833}
834
836 Texture *extra, Shader *shader, float x, float y, float w,
837 float h, const Color &tint) {
838 if (!color) {
839 drawSolidRect(x, y, w, h, tint);
840 return;
841 }
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) {
845 TexturedBatch batch{color, depth, shader, BlendMode::Opaque, Batcher{}};
846 batch.motion = motion;
847 batch.extra = extra;
848 texturedBatches.push_back(std::move(batch));
849 }
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));
852}
853
855 Texture *extra, Texture *specular, Shader *shader, float x,
856 float y, float w, float h, const Color &tint) {
857 if (!color) {
858 drawSolidRect(x, y, w, h, tint);
859 return;
860 }
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) {
865 TexturedBatch batch{color, depth, shader, BlendMode::Opaque, Batcher{}};
866 batch.motion = motion;
867 batch.extra = extra;
868 batch.specular = specular;
869 texturedBatches.push_back(std::move(batch));
870 }
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));
873}
874
876 Texture *displacement, float cx, float cy, float w, float h, float degrees, float u0, float v0,
877 float u1, float v1, float strengthPixels, float opacity, bool rotatedUV) {
879 if (!displacement || !scene || !scene->gpuHandle || !particleDistortionPipeline)
881
882 TexturedBatch batch{scene, displacement, nullptr, BlendMode::Alpha, Batcher{}};
883 batch.effect = TexturedBatch::Effect::SceneColorDistortion;
884 batch.batch.addTexturedRectRotated(cx, cy, w, h, degrees,
885 Color(strengthPixels, 0.f, 0.f, opacity), u0, v0, u1, v1,
886 rotatedUV);
887 texturedBatches.push_back(std::move(batch));
888 // Distortion samples scene color but does not replace the base scene
889 // composite; keep the automatic scene resolve underneath this overlay.
890 noteTexturedOverlay(nullptr, uint32_t(texturedBatches.size() - 1));
892}
893
894void Graphics::drawUiTextureRects(void* commandBuffer, const std::vector<UiTextureDraw>& draws,
895 std::size_t bufferOffset) {
896 if (!commandBuffer || draws.empty() || !uiTexturePipeline || uiColorWidth <= 0 ||
897 uiColorHeight <= 0)
898 return;
899
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));
904
905 for (const UiTextureDraw &draw : draws) {
906 if (!draw.texture || !draw.texture->gpuHandle || draw.w <= 0.f || draw.h <= 0.f)
907 continue;
908 auto *gpu = static_cast<GpuTexture *>(draw.texture->gpuHandle);
909 const vk::DescriptorSet set = gpu->descriptorSet;
910 if (!set) continue;
911
912 Batcher batch;
913 batch.addTexturedRect(draw.x, draw.y, draw.w, draw.h, draw.tint, draw.u0, draw.v0,
914 draw.u1, draw.v1);
915 batch.toNDC(uiColorWidth, uiColorHeight);
916 std::vector<TexturedVertex> vertices;
917 vertices.reserve(batch.vertices().size());
918 for (const auto &vertex : batch.vertices())
919 vertices.push_back(TexturedVertex{vertex.pos, vertex.color, vertex.uv});
920
921 while (bufferIndex >= buffers.size()) buffers.emplace_back();
922 vkb::HostVertexBuffer &vertexBuffer = buffers[bufferIndex++];
923 vertexBuffer.allocate<TexturedVertex>(frameToken(), device, vertices);
924
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,
936 nullptr);
937 const vk::DeviceSize offset = 0;
938 cb.bindVertexBuffers(0, 1, vertexBuffer, &offset);
939 cb.draw(uint32_t(vertices.size()), 1, 0, 0);
940 }
941}
942
943void Graphics::setLighting2D(const Lighting2DUBO &ubo) { lighting2dFrame = ubo; }
944
945void Graphics::ensureFlatNormalTexture() {
946 if (flatNormalTexture) return;
947 const uint8_t px[4] = {128, 128, 255, 255}; // flat normal pointing +Z
948 flatNormalTexture = newTexture(1, 1, px);
949}
950
951void Graphics::drawTexturedRectLitUV(Texture *albedo, Texture *normal, float x, float y, float w, float h, float u0,
952 float v0, float u1, float v1, const Color &color, BlendMode blend) {
953 if (!albedo) {
954 drawSolidRect(x, y, w, h, color, blend);
955 return;
956 }
957 ensureFlatNormalTexture();
958 if (!normal) normal = flatNormalTexture;
959 if (litBatches.empty() || litBatches.back().albedo != albedo || litBatches.back().normal != normal ||
960 litBatches.back().blend != blend) {
961 litBatches.push_back(LitBatch{albedo, normal, blend, Batcher{}});
962 }
963 litBatches.back().batch.addTexturedRect(x, y, w, h, color, u0, v0, u1, v1);
964 noteLitOverlay(uint32_t(litBatches.size() - 1));
965}
966
968 float degrees, float u0, float v0, float u1, float v1, const Color &color,
970 if (!albedo) {
972 return;
973 }
974 ensureFlatNormalTexture();
975 if (!normal) normal = flatNormalTexture;
976 if (litBatches.empty() || litBatches.back().albedo != albedo || litBatches.back().normal != normal ||
977 litBatches.back().blend != blend) {
978 litBatches.push_back(LitBatch{albedo, normal, blend, Batcher{}});
979 }
980 litBatches.back().batch.addTexturedRectRotated(cx, cy, w, h, degrees, color, u0, v0, u1, v1);
981 noteLitOverlay(uint32_t(litBatches.size() - 1));
982}
983
984vkb::BoundSet Graphics::lit2dSetFor(GpuTexture *albedo, GpuTexture *normal, bool offscreen) {
985 ASSERT(albedo != nullptr);
986 ASSERT(normal != nullptr);
987 auto &sets = offscreen ? offscreenLit2dSets : currentLit2dSets();
988 vkb::GenericBuffer &ubo = offscreen ? offscreenLighting2dUbo : currentLighting2dUbo();
989 LitSetKey key{albedo, normal};
990 auto it = sets.find(key);
991 if (it != sets.end()) return it->second;
992
993 vk::DescriptorSetAllocateInfo alloc{};
994 alloc.descriptorPool = descriptorPool;
995 alloc.descriptorSetCount = 1;
996 alloc.pSetLayouts = &lit2dSetLayout;
997 vkb::UnboundSet unbound{device->allocateDescriptorSets(alloc).front()};
998
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);
1008
1009 vkb::BoundSet bound = std::move(unbound).publish();
1010 sets.emplace(key, bound);
1011 return bound;
1012}
1013
1014vkb::BoundSet Graphics::post2SetFor(GpuTexture *color, GpuTexture *depth, GpuTexture *motion,
1015 GpuTexture *extra, GpuTexture *specular) {
1016 if (!color || !color->sampler) return {};
1017 vk::ImageView colorView = color->imageView();
1018 if (!colorView) return {};
1019 if (!depth || !depth->sampler || !depth->imageView()) depth = color;
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;
1023 PostSetKey key{color, depth, motion, extra, specular};
1024 auto it = post2Sets.find(key);
1025 if (it != post2Sets.end()) return it->second;
1026
1027 auto sets = vkb::DescriptorSetBuilder().layout(texSetLayout).build(device.instance, descriptorPool);
1028
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);
1045 return bound;
1046}
1047
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,
1050 bool hdr) {
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));
1056
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);
1066
1067 Batcher ndc = lb.batch;
1068 ndc.toNDC(viewW, viewH);
1069 std::vector<TexturedVertex> gpuVerts;
1070 gpuVerts.reserve(ndc.vertices().size());
1071 for (const auto &v : ndc.vertices()) {
1072 gpuVerts.push_back(TexturedVertex{v.pos, v.color, v.uv});
1073 }
1074
1075 if (texBufIndex >= texBufs.size()) texBufs.emplace_back();
1076 vkb::HostVertexBuffer &vb = texBufs[texBufIndex++];
1077 vb.allocate<TexturedVertex>(frameToken(), device, gpuVerts);
1078
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,
1082 nullptr);
1083 vk::DeviceSize offset = 0;
1084 cb.bindVertexBuffers(0, 1, vb, &offset);
1085 cb.draw(uint32_t(gpuVerts.size()), 1, 0, 0);
1086 }
1087}
1088
1089
1090Shader *Graphics::newShaderFromSpv(const std::vector<uint32_t> &vertSpv,
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");
1095
1096 std::vector<uint32_t> vert = vertSpv;
1097 if (vert.empty())
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");
1101
1102 auto gpu = std::make_unique<GpuShader>();
1103 gpu->pipelineLayout = shaderPipelineLayout;
1104 gpu->swapchainPipeline =
1105 createTexturedStylePipeline(vert, fragSpv, renderpass, shaderPipelineLayout);
1106 gpu->swapchainOpaquePipeline = createTexturedStylePipeline(
1107 vert, fragSpv, renderpass, shaderPipelineLayout, BlendMode::Opaque);
1108 if (offscreenRenderPass) {
1109 gpu->offscreenPipeline =
1110 createTexturedStylePipeline(vert, fragSpv, offscreenRenderPass, shaderPipelineLayout);
1111 gpu->offscreenOpaquePipeline = createTexturedStylePipeline(
1112 vert, fragSpv, offscreenRenderPass, shaderPipelineLayout, BlendMode::Opaque);
1113 }
1114
1115 auto sh = std::make_unique<Shader>();
1116 sh->setSpirv(std::move(vert), fragSpv);
1117 sh->gpuHandle = gpu.get();
1118
1119 Shader *raw = sh.get();
1120 ownedShaders.push_back(std::move(sh));
1121 ownedGpuShaders.push_back(std::move(gpu));
1122 return raw;
1123}
1124
1125Shader *Graphics::newShaderFromSpvFile(const std::string &vertPath, const std::string &fragPath) {
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);
1130 return newShaderFromSpv(vert, frag);
1131}
1132
1133Shader *Graphics::newShader(const std::string &vertGlsl, const std::string &fragGlsl) {
1134 if (fragGlsl.empty()) throw Exception("newShader: empty fragment GLSL");
1135 std::vector<uint32_t> vert;
1136 if (!vertGlsl.empty()) vert = compileShaderStage(vertGlsl, "vert", GlslStage::eVertex);
1137 auto frag = compileShaderStage(fragGlsl, "frag", GlslStage::eFragment);
1138 return newShaderFromSpv(vert, frag);
1139}
1140
1141Shader *Graphics::newMeshShaderFromSpv(const std::vector<uint32_t> &vertSpv,
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");
1148
1149 std::vector<uint32_t> vert = vertSpv;
1150 std::vector<uint32_t> frag = fragSpv;
1151 if (vert.empty())
1152 vert.assign(mesh3d_vert_spv, mesh3d_vert_spv + mesh3d_vert_spv_count);
1153 if (frag.empty())
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");
1157
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());
1164 // Built here, not lazily in drawMeshShader: vkCreateGraphicsPipelines
1165 // during an open render pass crashes software ICDs (Lavapipe).
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);
1174
1175 auto sh = std::make_unique<Shader>();
1176 sh->setKind(Shader::Kind::eMesh3D);
1177 sh->setSpirv(std::move(vert), std::move(frag));
1178 sh->gpuHandle = gpu.get();
1179 gpu->owner = sh.get();
1180
1181 Shader *raw = sh.get();
1182 ownedShaders.push_back(std::move(sh));
1183 ownedGpuShaders.push_back(std::move(gpu));
1184 return raw;
1185}
1186
1187Shader *Graphics::newHairShaderFromSpv(const std::vector<uint32_t> &vertSpv,
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");
1195
1196 std::vector<uint32_t> vert = vertSpv;
1197 if (vert.empty())
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");
1201
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);
1215
1216 auto sh = std::make_unique<Shader>();
1217 sh->setKind(Shader::Kind::eMesh3D);
1218 sh->setSpirv(std::move(vert), fragSpv);
1219 sh->gpuHandle = gpu.get();
1220 gpu->owner = sh.get();
1221
1222 Shader *raw = sh.get();
1223 ownedShaders.push_back(std::move(sh));
1224 ownedGpuShaders.push_back(std::move(gpu));
1225 return raw;
1226}
1227
1228Shader *Graphics::newHairShaderFromWgsl(const std::string &, const std::string &) {
1229 throw Exception("newHairShaderFromWgsl: WGSL is only supported on the WebGPU backend");
1230}
1231
1233 if (!shader || !shader->gpuHandle) return false;
1234
1235 auto *gpu = static_cast<GpuShader *>(shader->gpuHandle);
1236 auto gpuIt = std::find_if(ownedGpuShaders.begin(), ownedGpuShaders.end(),
1237 [&](const std::unique_ptr<GpuShader> &g) {
1238 return g.get() == gpu;
1239 });
1240 if (gpuIt == ownedGpuShaders.end()) return false;
1241
1242 auto shIt = std::find_if(ownedShaders.begin(), ownedShaders.end(),
1243 [&](const std::unique_ptr<Shader> &s) {
1244 return s.get() == shader;
1245 });
1246 if (shIt == ownedShaders.end()) return false;
1247
1248 // Mirror ~Graphics: pipelines are raw handles that must be destroyed here;
1249 // pipelineLayout is shared and must not be destroyed per-shader.
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);
1266 // Transfer the CPU facade to the caller instead of destroying it.
1267 (void)shIt->release();
1268 ownedShaders.erase(shIt);
1269 return true;
1270}
1271
1272Shader *Graphics::newMeshShader(const std::string &vertGlsl, const std::string &fragGlsl) {
1273 if (fragGlsl.empty()) throw Exception("newMeshShader: empty fragment GLSL");
1274 std::vector<uint32_t> vert;
1275 if (!vertGlsl.empty()) vert = compileShaderStage(vertGlsl, "vert", GlslStage::eVertex);
1276 auto frag = compileShaderStage(fragGlsl, "frag", GlslStage::eFragment);
1278}
1279
1280Shader *Graphics::newMeshShaderFromWgsl(const std::string &, const std::string &) {
1281 throw Exception("newMeshShaderFromWgsl: WGSL mesh shaders are only supported on the "
1282 "WebGPU backend; use newMeshShaderFromSpv on Vulkan.");
1283}
1284
1285Shader *Graphics::newShaderFromWgsl(const std::string &, const std::string &) {
1286 throw Exception("newShaderFromWgsl: WGSL shaders are only supported on the WebGPU "
1287 "backend; use newShaderFromSpv on Vulkan.");
1288}
1289
1290void Graphics::flushBatch() {
1292 if (!initialized) return;
1293 if (isCanvasActive()) {
1294 auto *oc = dynamic_cast<OffscreenCanvas *>(activeCanvas);
1295 if (!oc) throw Exception("flushBatch: active canvas is not an OffscreenCanvas");
1296 flushToOffscreen(oc);
1297 } else {
1298 flushToSwapchain();
1299 }
1300}
1301
1302void Graphics::abortOpen3DFrame() {
1303 const bool hadScene = sceneColorPassOpen;
1304 const bool had3D = swapchainPassOpen;
1305 try {
1306 if (hadScene) endSceneColorRenderPass();
1307 } catch (...) {
1308 sceneColorPassOpen = false;
1309 }
1310 try {
1311 if (had3D) {
1312 // Scene-pass path never opened the swapchain pass; open a dummy
1313 // one so the acquired command buffer can be submitted.
1314 if (hadScene) beginSwapchainColorPass();
1315 presentRecording = swapchainPass.endRenderPass();
1316 swapchainPass = {};
1317 const bool captured = screenReadbackEnabled
1318 ? recordSwapchainReadback(presentRecording.commandBuffer())
1319 : false;
1320 presentRecording.end().submitAndPresent();
1321 presentRecording = {};
1322 if (captured) {
1323 hasPresentedFrame = true;
1324 readbackReady = true;
1325 readbackCpuSynced = false;
1326 }
1327 }
1328 } catch (...) {
1329 swapchainPass = {};
1330 presentRecording = {};
1331 }
1332 swapchainPassOpen = false;
1333 sceneColorPassOpen = false;
1334 frameHad3D = false;
1335 hasPendingClear = false;
1336 flushingSwapchain_ = false;
1337 clear2DBatches();
1338}
1339
1340void Graphics::flushToSwapchain() {
1341 if (flushingSwapchain_) return;
1342 flushingSwapchain_ = true;
1343 bool completed = false;
1344 struct FlushGuard {
1345 Graphics *g;
1346 bool *completed;
1347 ~FlushGuard() {
1348 g->flushingSwapchain_ = false;
1349 if (!*completed) g->abortOpen3DFrame();
1350 }
1351 } guard{this, &completed};
1352
1353 const bool continue3D = swapchainPassOpen;
1354 const bool hadScenePass = sceneColorPassOpen;
1355 const bool hasScenePath = hadScenePass || pendingSceneResolveSource != nullptr;
1356
1357 if (hadScenePass) {
1358 endSceneColorRenderPass();
1359 queueSceneColorResolve();
1360 }
1361
1362 if (!continue3D) {
1363 // 2D-only path: acquire the present CB and record deferred passes now,
1364 // so the UI overlay's dedicated MSAA pass can be recorded before the
1365 // swapchain pass begins.
1366 if (!beginPresentCommandBuffer()) {
1367 dropPendingOffscreenPasses();
1368 hasPendingClear = false;
1369 completed = true;
1370 return;
1371 }
1372 recordDeferredFrameGraph();
1373 }
1374
1375 if (!(continue3D && !hasScenePath))
1376 recordGpuParticleCompute(presentRecording.commandBuffer());
1377
1378 materializeSceneColorResolve();
1379
1380 // Render the UI overlay (ImGui) into its own MSAA pass, resolved and
1381 // composited as the top-most fullscreen quad. Skipped only on the rare 3D
1382 // fallback path where the swapchain pass is already open from begin3DFrame.
1383 if (presentOverlayFn_ && !(continue3D && !hasScenePath)) {
1384 renderUiOverlayPass();
1385 }
1386
1387 // HDR present: compose scene + overlays in paper-white-relative linear space,
1388 // then encode once into the swapchain. SDR (and the rare already-open 3D
1389 // path) keep drawing directly into the present pass.
1390 const bool useHdrCompose =
1391 isDisplayHdrActive() && (hasScenePath || !continue3D) && !swapchainPassOpen;
1392 if (useHdrCompose) {
1393 if (!beginPresentComposePass()) {
1394 // Compose target unavailable — fall back to direct swapchain draws.
1395 beginSwapchainColorPass();
1396 swapchainPassOpen = true;
1397 }
1398 } else if (hasScenePath || !continue3D) {
1399 beginSwapchainColorPass();
1400 swapchainPassOpen = true;
1401 }
1402
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;
1412 Texture *sceneTex = getSceneColorTexture();
1413 clear2DBatches();
1414
1415 auto &cb = currentPresentCb();
1416 setViewportAndScissor(cb, swapchain.extent.width, swapchain.extent.height);
1417
1418 // Persistent per-frame-slot buffers (see currentFrame2DBuffers). Safe to
1419 // overwrite: acquireForFrame() already waited this slot's fence.
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;
1424
1425 auto swapchainTexPipe = [&](BlendMode mode) -> vk::Pipeline {
1426 if (presentComposeActive_) {
1427 switch (mode) {
1429 return hdrOffscreenAdditiveTexPipeline;
1431 return hdrOffscreenPremultipliedTexPipeline;
1433 return hdrOffscreenMultiplyTexPipeline;
1434 case BlendMode::Opaque:
1435 return hdrOffscreenOpaqueTexPipeline;
1436 case BlendMode::Alpha:
1437 default:
1438 return hdrOffscreenTexPipeline;
1439 }
1440 }
1441 switch (mode) {
1443 return additiveTexPipeline;
1445 return premultipliedTexPipeline;
1447 return multiplyTexPipeline;
1448 case BlendMode::Opaque:
1449 return opaqueTexPipeline;
1450 case BlendMode::Alpha:
1451 default:
1452 return texPipeline;
1453 }
1454 };
1455 auto swapchainSolidPipe = [&](BlendMode mode) -> vk::Pipeline {
1456 if (presentComposeActive_) {
1457 switch (mode) {
1459 return hdrOffscreenAdditiveSolidPipeline;
1461 return hdrOffscreenPremultipliedSolidPipeline;
1463 return hdrOffscreenMultiplySolidPipeline;
1464 case BlendMode::Alpha:
1465 return hdrOffscreenSolidAlphaPipeline;
1466 case BlendMode::Opaque:
1467 default:
1468 return hdrOffscreenSolidPipeline;
1469 }
1470 }
1471 switch (mode) {
1473 return additiveSolidPipeline;
1475 return premultipliedSolidPipeline;
1477 return multiplySolidPipeline;
1478 case BlendMode::Alpha:
1479 return solidAlphaPipeline;
1480 case BlendMode::Opaque:
1481 default:
1482 return pipeline;
1483 }
1484 };
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);
1493
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)) {
1501 auto *depthGpu =
1502 depthTexture ? static_cast<GpuTexture *>(depthTexture->gpuHandle) : nullptr;
1503 auto *motionGpu =
1504 tb.motion ? static_cast<GpuTexture *>(tb.motion->gpuHandle) : nullptr;
1505 auto *extraGpu = tb.extra ? static_cast<GpuTexture *>(tb.extra->gpuHandle) : nullptr;
1506 auto *specularGpu =
1507 tb.specular ? static_cast<GpuTexture *>(tb.specular->gpuHandle) : nullptr;
1508 if (vk::DescriptorSet combo =
1509 post2SetFor(gpu, depthGpu, motionGpu, extraGpu, specularGpu))
1510 texSet = combo;
1511 }
1512 Batcher ndc = tb.batch;
1513 ndc.toNDC(width, height);
1514 std::vector<TexturedVertex> gpuVerts;
1515 gpuVerts.reserve(ndc.vertices().size());
1516 for (const auto &v : ndc.vertices())
1517 gpuVerts.push_back(TexturedVertex{v.pos, v.color, v.uv});
1518
1519 if (texBufIndex >= texBufs.size()) texBufs.emplace_back();
1520 vkb::HostVertexBuffer &vb = texBufs[texBufIndex++];
1521 vb.allocate<TexturedVertex>(frameToken(), device, gpuVerts);
1522
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,
1531 nullptr);
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);
1537 customPipeline = tb.blend == BlendMode::Opaque && gs->hdrOffscreenOpaquePipeline
1538 ? gs->hdrOffscreenOpaquePipeline
1539 : gs->hdrOffscreenPipeline;
1540 } else {
1541 customPipeline = tb.blend == BlendMode::Opaque && gs->swapchainOpaquePipeline
1542 ? gs->swapchainOpaquePipeline
1543 : gs->swapchainPipeline;
1544 }
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,
1551 Shader::kPushConstantBytes, tb.shader->pushConstantData());
1552 } else {
1553 vk::Pipeline pipe = toneMapScene ? tonemapPipe : swapchainTexPipe(tb.blend);
1554 if (!pipe) return;
1555 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, pipe);
1556 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, texPipelineLayout, 0, 1,
1557 &texSet, 0, nullptr);
1558 }
1559 vk::DeviceSize offset = 0;
1560 cb.bindVertexBuffers(0, 1, vb, &offset);
1561 cb.draw(uint32_t(gpuVerts.size()), 1, 0, 0);
1562 };
1563
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);
1568 if (!pipe) return;
1569 Batcher ndc = solid[idx].batch;
1570 ndc.toNDC(width, height);
1571 std::vector<ColorVertex> gpuVerts;
1572 gpuVerts.reserve(ndc.vertices().size());
1573 for (const auto &v : ndc.vertices())
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;
1578 };
1579
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);
1583 if (!pipe) return;
1584 uploadSolid(batchIndex);
1585 vk::DeviceSize offset = 0;
1586 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, pipe);
1587 cb.bindVertexBuffers(0, 1, solidBufs[batchIndex], &offset);
1588 cb.draw(count, 1, begin, 0);
1589 };
1590
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]);
1604 }
1605 }
1606 };
1607
1608 bool engineDrawn = false;
1609 auto drawEngine3D = [&]() {
1610 if (engineDrawn) return;
1611 engineDrawn = true;
1612 replaySpans(engineSpans);
1613 };
1614
1615 auto drawPlacedSceneResolve = [&](TexturedBatch &placed) {
1616 if (!sceneResolve) {
1617 drawTextured(placed);
1618 return;
1619 }
1620 TexturedBatch blit = *sceneResolve;
1621 const glm::vec4 acesColor = sceneResolve->batch.vertices().empty()
1622 ? glm::vec4(1.f, 1.f, 1.f, 65536.f)
1623 : sceneResolve->batch.vertices().front().color;
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;
1631 blit.batch.clear();
1632 blit.batch.addTexturedRect(x, y, w, h,
1633 Color(tint.r * acesColor.r, tint.g * acesColor.g,
1634 tint.b * acesColor.b, acesColor.a),
1635 src[0].uv.x, src[0].uv.y, src[1].uv.x, src[2].uv.y);
1636 }
1637 drawTextured(blit, true);
1638 };
1639
1640 // Default: blit 3D fullscreen under script 2D. Scripts that call
1641 // drawScene3D / drawTexturedRect(getSceneColorTexture()) own the order.
1642 if (autoScene && sceneResolve) {
1643 drawTextured(*sceneResolve, true);
1644 drawEngine3D();
1645 }
1646
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()));
1651 }
1652 if (texPipeline || hdrOffscreenTexPipeline) {
1653 for (auto &tb : textured) drawTextured(tb);
1654 }
1655 if (litAvailable) drawLitBatches(cb, width, height, lit, texBufs, texBufIndex, false);
1656 } else {
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()) {
1662 // Distortion overlays sample scene color; they are not
1663 // drawScene3D placements. Replacing them with the ACES
1664 // resolve skips particleDistortionPipeline and can cover
1665 // the autoScene blit with a near-empty frame.
1666 const bool placedScene =
1667 sceneResolve && textured[sp.index].texture == sceneTex &&
1668 textured[sp.index].effect != TexturedBatch::Effect::SceneColorDistortion;
1669 if (placedScene) {
1670 drawPlacedSceneResolve(textured[sp.index]);
1671 } else {
1672 drawTextured(textured[sp.index]);
1673 }
1674 if (placedScene) {
1675 drawEngine3D();
1676 }
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]);
1683 }
1684 }
1685 }
1686
1687 if (uiResolve) drawTextured(*uiResolve);
1688
1689 // Invalidate prior readback so a failed present cannot reuse a stale frame.
1690 if (screenReadbackEnabled) hasPresentedFrame = false;
1691
1692 // Fallback path only: the swapchain pass was already open when this frame
1693 // began (3D MSAA scene pass unavailable), so draw the overlay directly.
1694 if (presentOverlayFn_ && continue3D && !hadScenePass) {
1695 VkCommandBuffer raw = static_cast<VkCommandBuffer>(cb);
1697 }
1698
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);
1706 }
1707
1708 presentRecording = swapchainPass.endRenderPass();
1709 swapchainPass = {};
1710 const bool captured =
1711 screenReadbackEnabled ? recordSwapchainReadback(presentRecording.commandBuffer()) : false;
1712 writeGpuTimestampEnd();
1713 presentRecording.end().submitAndPresent();
1714 readGpuFrameTiming();
1715 presentRecording = {};
1716 if (captured) {
1717 hasPresentedFrame = true;
1718 readbackReady = true;
1719 readbackCpuSynced = false;
1720 }
1721 hasPendingClear = false;
1722 swapchainPassOpen = false;
1723 frameHad3D = false;
1724 completed = true;
1725}
1726
1727
1728} // namespace eve::graphics::vulkan
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
const std::string & s
float cx
Definition CardTypes.cpp:33
float cy
Definition CardTypes.cpp:34
float degrees
Definition CardTypes.cpp:35
float uv
graphics::Texture * albedo
tensor::Graph g
Definition GpuGraph.cpp:7
vkb::Device & device
vk::ShaderModule vert
vk::ShaderModule frag
std::uint32_t key
vk::UniqueSampler sampler
vk::UniqueImage image
glm::vec4 tint
float u
Definition Grass.cpp:233
glm::vec3 n
Definition Grass.cpp:63
double r
float v
std::int32_t c
float blend
int h
std::vector< Colorf > px
size_t offset
std::uint64_t bytes
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< TriangleRef > triangles
MeshVfxBatchedDraw draw
float tb
Texture * normal
std::string error
Definition Package.cpp:60
int idx
std::vector< Point > vertices
std::string path
Definition PlayHost.cpp:110
std::weak_ptr< PrimitiveScene > scene
float begin
float d
uint8_t * pixels
glm::mat4 viewProj
Shader * shader
Color clearColor
bool repeatV
bool repeatU
std::uint32_t count
bool placed
float opacity
float size
Definition TreeMesh.cpp:156
const UnitySourceAsset & source
std::uint32_t depth
float m[16]
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
Accumulates solid / textured quads in logical (Y-down) coordinates. Used by RenderSystem; not a publi...
Definition Batcher.h:24
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.
Definition Batcher.cpp:25
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).
Definition Batcher.cpp:52
void toNDC(int logicalW, int logicalH)
To ndc.
Definition Batcher.cpp:119
const std::vector< BatchVertex > & vertices() const
Vertices.
Definition Batcher.h:46
Canvas public API.
Definition Canvas.h:17
virtual void clear(std::optional< Color > color, std::optional< int > stencil, std::optional< double > depth)=0
Clears .
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),...
Definition Graphics.h:1081
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.
Definition Graphics.h:502
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).
Definition Graphics.h:2249
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...
Definition Graphics.h:1091
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.
Definition Graphics.h:796
SceneColorDistortionStatus
Refract the resolved 3D scene color through a displacement texture.
Definition Graphics.h:793
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.
Definition Graphics.h:1992
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.
Definition Graphics.h:1908
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...
Definition Graphics.h:1047
PresentOverlayFn presentOverlayFn_
Definition Graphics.h:2259
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.
Definition Graphics.h:504
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.
Custom GPU program.
Definition Shader.h:39
static constexpr uint32_t kPushConstantBytes
Definition Shader.h:43
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:18
EVENGINE_API_BACKENDS public API.
Definition Graphics.h:342
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.
Definition Canvas.h:15
Represents raw pixel data.
Definition ImageData.h:38
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...
Definition Color.h:13
MipmapMode
Mipmap filter; Disabled turns off mip sampling (maxLod clamped to 0).
eve::BlendMode BlendMode
Compatibility alias for the shared 2D blend mode.
Definition BlendMode.h:8
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...
Definition Widget.cpp:715
BlendMode
Render-neutral 2D blend mode shared by graphics-facing modules.
Definition RenderTypes.h:11
Lighting2DUBO public API.
Definition Light.h:53
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...
GpuMesh public API.
Definition Graphics.h:289
GpuShader public API.
Definition Graphics.h:322
GpuTexture public API.
Definition Graphics.h:257
TexturedVertex public API.
Definition Graphics.h:98
Backend-internal textured rectangle queued by a UI presenter.
Definition Graphics.h:122
glm::vec4 color