载入中...
搜索中...
未找到
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
8#include "graphics/Light.h"
11
12#include <SDL2/SDL.h>
13#include <SDL2/SDL_vulkan.h>
14
15#include <algorithm>
16#include <array>
17#include <cmath>
18#include <cstdio>
19#include <cstdlib>
20#include <cstdint>
21#include <cstring>
22#include <functional>
23#include <stdexcept>
24#include <string>
25#include <vector>
26#if !defined(_WIN32)
27#include <unistd.h>
28#endif
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
57void Graphics::ensurePresentCaptureHook() {
58 // Screen readback is recorded inline into the present command buffer (see
59 // flushToSwapchain / abortOpen3DFrame), so the present model must never
60 // get a post-submit hook: drawFrame() waits for this frame's fence
61 // whenever the hook is set, which serializes every frame and erases the
62 // multi-frame overlap we rely on for async rendering.
63 presentModel.after_render_before_present = nullptr;
64}
65
66void Graphics::ensureReadbackSlots() {
67 if (pixelWidth <= 0 || pixelHeight <= 0) return;
68 const size_t bytes = size_t(pixelWidth) * size_t(pixelHeight) * 4;
69 const size_t want = std::max<size_t>(2, frameSlotCount());
70 if (!screenReadbackSlots.empty() && screenReadbackSlots.size() >= want &&
71 screenReadbackBytes == bytes)
72 return;
73 // Recreating the staging ring must never race in-flight copies. Callers
74 // run on the render thread, and a size change implies the swapchain was
75 // rebuilt under waitIdle (rebuildSwapchainIfNeeded / recreate path).
76 for (auto &slot : screenReadbackSlots) {
77 if (slot.mapped) {
78 device->unmapMemory(slot.staging.memory);
79 slot.mapped = nullptr;
80 }
81 slot.staging.release();
82 }
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);
90 }
91 screenReadbackBytes = bytes;
92 readbackReady = false;
93 readbackCpuSynced = false;
94 readbackWriteSlot = 0;
95}
96
97bool Graphics::recordSwapchainReadback(vk::CommandBuffer cb) {
98 if (!screenReadbackEnabled || pixelWidth <= 0 || pixelHeight <= 0) return false;
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;
104 readbackBgra = bgra;
105
106 const uint32_t imageIndex = presentModel.acquired_image_index;
107 auto &images = swapchain.get_images();
108 if (imageIndex >= images.size()) return false;
109
110 ensureReadbackSlots();
111 if (screenReadbackSlots.empty()) return false;
112
113 const size_t slot = size_t(presentRecording.slot().index) % screenReadbackSlots.size();
114 readbackWriteSlot = slot;
115 const vk::Image image = images[imageIndex];
116 // Recorded at the end of the present command buffer: the swapchain render
117 // pass has already transitioned the image back to PresentSrcKHR, and the
118 // copy runs in the same submit+present — no extra queue submit, no
119 // waitIdle, no per-frame buffer allocation.
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};
127 // The render pass has already transitioned the image to PresentSrcKHR, but
128 // there is no fence between it and this copy — the barrier must order the
129 // render-pass color writes against the transfer explicitly.
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,
134 &toTransfer);
135
136 vk::BufferImageCopy region{};
137 region.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
138 region.imageExtent = vk::Extent3D{uint32_t(pixelWidth), uint32_t(pixelHeight), 1};
139 cb.copyImageToBuffer(image, vk::ImageLayout::eTransferSrcOptimal,
140 screenReadbackSlots[slot].staging.buffer, region);
141
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,
149 &toPresent);
150 return true;
151}
152
153void Graphics::syncReadbackCpu() {
154 if (!readbackReady || screenReadbackSlots.empty()) return;
155 if (readbackCpuSynced && !lastFrameRgba.empty()) return;
156 if (readbackWriteSlot >= screenReadbackSlots.size()) return;
157
158 const size_t bytes = screenReadbackBytes;
159 if (bytes == 0) return;
160
161 // The newest copy lives in the present submission of this frame slot; wait
162 // only that slot's fence instead of a device-wide waitIdle.
163 presentModel.waitForFrameSlot(readbackWriteSlot);
164
165 auto &slot = screenReadbackSlots[readbackWriteSlot];
166 if (!slot.mapped)
167 slot.mapped = device->mapMemory(slot.staging.memory, 0, vk::DeviceSize(bytes));
168
169 lastFrameRgba.resize(bytes);
170 if (readbackBgra) {
171 // BGRA -> RGBA byte swap, two pixels per 64-bit word (masked swap).
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);
180 }
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);
186 }
187 } else {
188 std::memcpy(lastFrameRgba.data(), slot.mapped, bytes);
189 }
190 readbackCpuSynced = true;
191}
192
193void Graphics::destroyReadbackResources() {
194 for (auto &slot : screenReadbackSlots) {
195 if (slot.mapped) {
196 device->unmapMemory(slot.staging.memory);
197 slot.mapped = nullptr;
198 }
199 slot.staging.release();
200 }
201 screenReadbackSlots.clear();
202 screenReadbackBytes = 0;
203 readbackReady = false;
204 readbackCpuSynced = false;
205 hasPresentedFrame = false;
206}
207
209 syncReadbackCpu();
210 if (!hasPresentedFrame || lastFrameRgba.empty())
211 throw Exception("Graphics::newImageData: no presented frame");
212 auto *img = new image::ImageData(pixelWidth, pixelHeight, "RGBA8");
213 std::memcpy(img->getData(), lastFrameRgba.data(), lastFrameRgba.size());
214 return img;
215}
216
218 syncReadbackCpu();
219 if (!hasPresentedFrame || lastFrameRgba.empty())
220 throw Exception("Graphics::getPixel: no presented frame");
221 if (x < 0 || y < 0 || x >= width || y >= height)
222 throw Exception("Graphics::getPixel: out of bounds (%d,%d)", x, y);
223
224 const int pxX = (width > 0) ? int((int64_t(x) * pixelWidth) / width) : x;
225 const int pxY = (height > 0) ? int((int64_t(y) * pixelHeight) / height) : y;
226 const int cx = std::min(std::max(pxX, 0), pixelWidth - 1);
227 const int cy = std::min(std::max(pxY, 0), pixelHeight - 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;
233 // If surface ended up sRGB, convert encoded bytes back to linear Color space.
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);
238 };
239 r = toLinear(r);
240 g = toLinear(g);
241 b = toLinear(b);
242 }
243 return Color(r, g, b, a);
244}
245
247 const std::vector<eve::graphics::Graphics::EntityIdDraw> &draws, const glm::mat4 &viewProj,
248 int width, int height) {
249 if (!initialized || width <= 0 || height <= 0) return nullptr;
250 // G-buffer pipeline / render pass are created lazily by createGBufferResources,
251 // so that must run before the availability check.
252 createGBufferResources(width, height);
253 if (!gbufferPipeline || !gbufferRenderPass) return nullptr;
254 auto *slot = currentGBufferSlot();
255 if (!slot || !slot->framebuffer || !whiteTexture) return nullptr;
256
257 // Render each mesh with a flat idColor into the G-buffer albedo attachment
258 // (location 2) by reusing the G-buffer pipeline (its fragment shader writes
259 // outAlbedo = albedo * tint, so passing a white texture + idColor tint gives
260 // a per-pixel flat entity-ID color).
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));
265 };
266 for (const auto &d : draws) {
267 if (!d.mesh || !d.mesh->gpuHandle) continue;
268 GBufferDraw gd{};
269 gd.mesh = d.mesh;
270 gd.albedo = whiteTexture;
271 gd.push.mvp = viewProj * d.model;
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);
279 }
280 if (idDraws.empty()) return nullptr;
281
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);
288
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) {
313 auto *gpuTex = static_cast<GpuTexture *>(whiteTexture->gpuHandle);
314 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
315 gbufferPipelineLayout, 0, 1,
316 gpuTex->descriptorSet.ptr(), 0, nullptr);
317 }
318 cb.pushConstants(gbufferPipelineLayout,
319 vk::ShaderStageFlagBits::eVertex |
320 vk::ShaderStageFlagBits::eFragment,
321 0, sizeof(GBufferPush), &d.push);
322 drawIndexedMesh(cb, *gpuMesh);
323 }
324 cb.endRenderPass();
325 slot->normal.endSampledLayout();
326 slot->depthColor.endSampledLayout();
327 slot->albedo.endSampledLayout();
328 slot->depth.endSampledLayout();
329
330 // Copy the albedo attachment (location 2 = ID colors) to CPU.
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,
337 region);
338 slot->albedo.setLayout(cb, vk::ImageLayout::eShaderReadOnlyOptimal);
339 });
340
341 auto *img = new image::ImageData(int(w), int(h), "RGBA8");
342 void *mapped = device->mapMemory(staging.memory, 0, byteSize);
343 std::memcpy(img->getData(), mapped, size_t(byteSize));
344 device->unmapMemory(staging.memory);
345 staging.release();
346
347 // 让 RenderControl 的 GBuffer 也指向该槽位(镜像 endGBufferPass),这样
348 // 上层可通过 getDepthTexture()/getNormalTexture() 读取本次离屏 ID 渲染
349 // 生成的深度/法线(供 capture_render_frame 的 depth/normal 复用)。
350 if (RenderControl *rc = getRenderControl()) {
351 rc->getGBuffer()->setTargets(int(w), int(h), &slot->depthColorTex, &slot->normalTex,
352 &slot->albedoTex, &slot->depthTex);
353 }
354 return img;
355}
356
358 if (!initialized) return nullptr;
359 auto *slot = currentGBufferSlot();
360 if (!slot) return nullptr;
361 vkb::ColorTarget *src = nullptr;
362 if (name == "depth")
363 src = &slot->depthColor; // RGBA8 linear depth
364 else if (name == "normal")
365 src = &slot->normal;
366 else if (name == "albedo")
367 src = &slot->albedo;
368 else
369 return nullptr;
370
371 const uint32_t w = uint32_t(gbufferWidth);
372 const uint32_t h = uint32_t(gbufferHeight);
373 if (w == 0 || h == 0) return nullptr;
374
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);
388 });
389
390 auto *img = new image::ImageData(int(w), int(h), "RGBA8");
391 void *mapped = device->mapMemory(staging.memory, 0, byteSize);
392 std::memcpy(img->getData(), mapped, size_t(byteSize));
393 device->unmapMemory(staging.memory);
394 staging.release();
395 return img;
396}
397
399 ASSERT(initialized);
400 ASSERT_GT(w, 0);
401 ASSERT_GT(h, 0);
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);
406 Canvas *raw = c.get();
407 ownedCanvases.push_back(std::move(c));
408 return raw;
409}
410
412 Canvas *next = canvas;
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();
419 activeCanvas = next;
420}
421
423 return activeCanvas != nullptr;
424}
425
427 return activeCanvas ? activeCanvas : const_cast<Graphics *>(this);
428}
429
430void Graphics::setViewportSize(int newW, int newH, int newPw, int newPh) {
431 bool changed = (newW != width) || (newH != height) || (newPw != pixelWidth) || (newPh != pixelHeight);
432 width = newW;
433 height = newH;
434 pixelWidth = newPw;
435 pixelHeight = newPh;
436 if (initialized && changed) swapchainDirty = true;
437}
438
439void Graphics::clear2DBatches() {
440 solidBatches.clear();
441 texturedBatches.clear();
442 litBatches.clear();
443 overlaySpans.clear();
444 engine3DSpans.clear();
445 pendingSceneResolve.reset();
446 pendingUiResolve.reset();
447 sceneColorComposited = false;
448}
449
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());
454 auto &spans = recordingEngine3D_ ? engine3DSpans : overlaySpans;
455 if (!spans.empty() && spans.back().kind == OverlayKind::Solid &&
456 spans.back().index == idx) {
457 spans.back().vertCount = n - spans.back().vertBegin;
458 return;
459 }
460 const uint32_t begin = n >= 6u ? n - 6u : 0u;
461 spans.push_back({OverlayKind::Solid, idx, begin, n - begin});
462}
463
464void Graphics::noteTexturedOverlay(Texture *tex) {
465 if (tex && tex == getSceneColorTexture()) sceneColorComposited = true;
466 auto &spans = recordingEngine3D_ ? engine3DSpans : overlaySpans;
467 const uint32_t idx = texturedBatches.empty() ? 0u : uint32_t(texturedBatches.size() - 1);
468 if (!spans.empty() && spans.back().kind == OverlayKind::Textured && spans.back().index == idx)
469 return;
470 spans.push_back({OverlayKind::Textured, idx, 0, 0});
471}
472
473void Graphics::noteLitOverlay() {
474 auto &spans = recordingEngine3D_ ? engine3DSpans : overlaySpans;
475 const uint32_t idx = litBatches.empty() ? 0u : uint32_t(litBatches.size() - 1);
476 if (!spans.empty() && spans.back().kind == OverlayKind::Lit && spans.back().index == idx)
477 return;
478 spans.push_back({OverlayKind::Lit, idx, 0, 0});
479}
480
481void Graphics::clear(std::optional<Color> color, std::optional<int>, std::optional<double>) {
482 // Keep 3D framebuffer contents when composing 2D on top of an open 3D pass.
483 if (frameHad3D && activeCanvas == nullptr) return;
484 clearColor = color.value_or(backgroundColor);
485 hasPendingClear = true;
486 clear2DBatches();
487 if (auto *oc = dynamic_cast<OffscreenCanvas *>(activeCanvas)) {
488 oc->clear(clearColor, std::nullopt, std::nullopt);
489 }
490}
491
492void Graphics::drawSolidRect(float x, float y, float w, float h, const Color &color,
493 BlendMode blend) {
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;
499 }
500 it->batch.addRect(x, y, w, h, color);
501 noteSolidOverlay();
502}
503
504void Graphics::drawSolidRectRotated(float cx, float cy, float w, float h, float degrees,
505 const Color &color, BlendMode blend) {
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;
511 }
512 it->batch.addRectRotated(cx, cy, w, h, degrees, color);
513 noteSolidOverlay();
514}
515
516
517float Graphics::getMaxAnisotropy() const { return maxSamplerAnisotropy; }
518
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;
522 };
523 auto toFilter = [](FilterMode m) {
524 return m == FilterMode::Nearest ? vk::Filter::eNearest : vk::Filter::eLinear;
525 };
526 auto toMip = [](MipmapMode m) {
527 return m == MipmapMode::Nearest ? vk::SamplerMipmapMode::eNearest
528 : vk::SamplerMipmapMode::eLinear;
529 };
530
531 const bool useMips = sampler.mipmap != MipmapMode::Disabled && mipLevels > 1;
532 float maxLod = useMips ? std::min(sampler.maxLod, float(mipLevels - 1)) : 0.f;
533 if (maxLod < sampler.minLod) maxLod = sampler.minLod;
534
535 float aniso = 1.f;
536 bool enableAniso = false;
537 if (sampler.maxAnisotropy > 1.f && maxSamplerAnisotropy > 1.f) {
538 enableAniso = true;
539 aniso = std::min(sampler.maxAnisotropy, maxSamplerAnisotropy);
540 }
541
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)
553 .maxLod(maxLod)
554 .build(device);
555}
556
557void Graphics::writeCombinedImageDescriptor(GpuTexture *gpu) {
558 if (!gpu || !gpu->descriptorSet || !gpu->sampler) return;
559 vk::ImageView view = gpu->imageView();
560 if (!view) return;
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))
568 .update(device.instance);
569 gpu->descriptorSet = std::move(unbound).publish();
570}
571
572Texture *Graphics::newTexture(int w, int h, const uint8_t *rgba, bool repeatU, bool repeatV) {
574 info.sampler.repeatU = repeatU;
575 info.sampler.repeatV = repeatV;
576 return newTexture(w, h, rgba, info);
577}
578
579Texture *Graphics::newTexture(int w, int h, const uint8_t *rgba, const TextureCreateInfo &rawInfo) {
580 ASSERT(initialized);
581 ASSERT_GT(w, 0);
582 ASSERT_GT(h, 0);
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");
586
587 TextureCreateInfo info = normalizeTextureInfo(rawInfo);
588 const uint32_t mipLevels =
589 info.generateMipmaps ? uint32_t(mipmapCountForSize(w, h)) : 1u;
590
591 auto gpu = std::make_unique<GpuTexture>();
592 gpu->width = w;
593 gpu->height = h;
594 gpu->isCube = false;
595 gpu->mipLevels = mipLevels;
596 gpu->samplerState = info.sampler;
597 gpu->image = vkb::TextureImage2D(device, uint32_t(w), uint32_t(h), mipLevels);
598
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);
603
604 gpu->sampler = createVkSampler(info.sampler, mipLevels);
605
606 auto sets = vkb::DescriptorSetBuilder().layout(texSetLayout).build(device.instance, descriptorPool);
607
608 gpu->descriptorSet = vkb::BoundSet{sets[0]};
609 writeCombinedImageDescriptor(gpu.get());
610
611 auto tex = std::make_unique<Texture>();
612 tex->width = w;
613 tex->height = h;
614 tex->pixelWidth = w;
615 tex->pixelHeight = h;
616 tex->mipmapCount = int(mipLevels);
617 tex->sampler = info.sampler;
618 tex->gpuHandle = gpu.get();
619
620 Texture *raw = tex.get();
621 ownedTextures.push_back(std::move(tex));
622 ownedGpuTextures.push_back(std::move(gpu));
623 return raw;
624}
625
626Texture *Graphics::newCubemap(int faceSize, const uint8_t *rgbaFaces) {
627 // IBL shaders use textureLod(roughness * 5); generate mips by default.
628 return newCubemap(faceSize, rgbaFaces, TextureCreateInfo::withMipmaps(false));
629}
630
631Texture *Graphics::newCubemap(int faceSize, const uint8_t *rgbaFaces,
632 const TextureCreateInfo &rawInfo) {
633 ASSERT(initialized);
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");
638
639 TextureCreateInfo info = normalizeTextureInfo(rawInfo);
640 info.sampler.repeatU = false;
641 info.sampler.repeatV = false;
642 info.sampler.repeatW = false;
643 const uint32_t mipLevels =
644 info.generateMipmaps ? uint32_t(mipmapCountForSize(faceSize, faceSize)) : 1u;
645
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;
650 gpu->isCube = true;
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);
655
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);
660
661 gpu->sampler = createVkSampler(info.sampler, mipLevels);
662 // Cubemap sampled via mesh3d descriptor sets — no 2D texSetLayout binding required here.
663
664 auto tex = std::make_unique<Texture>();
665 tex->width = faceSize;
666 tex->height = faceSize;
667 tex->pixelWidth = faceSize;
668 tex->pixelHeight = faceSize;
669 tex->layers = 6;
670 tex->mipmapCount = int(mipLevels);
671 tex->sampler = info.sampler;
672 tex->gpuHandle = gpu.get();
673
674 Texture *raw = tex.get();
675 ownedTextures.push_back(std::move(tex));
676 ownedGpuTextures.push_back(std::move(gpu));
677 return raw;
678}
679
681 ASSERT(data != nullptr);
682 if (!data) throw Exception("newTexture: null ImageData");
683 if (data->getFormat() != "RGBA8")
684 throw Exception("newTexture: only RGBA8 ImageData supported for now");
685 return newTexture(data->getWidth(), data->getHeight(),
686 static_cast<const uint8_t *>(data->getData()));
687}
688
690 ASSERT(data != nullptr);
691 if (!data) throw Exception("newTexture: null ImageData");
692 if (data->getFormat() != "RGBA8")
693 throw Exception("newTexture: only RGBA8 ImageData supported for now");
694 return newTexture(data->getWidth(), data->getHeight(),
695 static_cast<const uint8_t *>(data->getData()), info);
696}
697
698
699void Graphics::setTextureSampler(Texture *texture, const TextureSampler &sampler) {
700 if (!texture || !texture->gpuHandle || !initialized) return;
701 for (auto &owned : ownedGpuTextures) {
702 if (owned.get() != texture->gpuHandle) continue;
703 // In-flight frames may still be sampling the old sampler / descriptor.
704 waitForSharedGpuResources();
705 if (owned->sampler) device->destroySampler(owned->sampler);
706 owned->samplerState = sampler;
707 owned->sampler = createVkSampler(sampler, owned->mipLevels);
708 texture->sampler = sampler;
709 if (!owned->isCube) writeCombinedImageDescriptor(owned.get());
710 invalidateTextureBindings();
711 return;
712 }
713}
714
716 if (!texture || !texture->gpuHandle) return false;
717 // Renderer-owned fallback textures must never be released by callers.
718 if (texture == whiteTexture || texture == flatNormalTexture ||
719 texture == flatNormalTexture3D || texture == defaultEnvCubemap)
720 return false;
721
722 auto *gpu = static_cast<GpuTexture *>(texture->gpuHandle);
723 auto gpuIt = std::find_if(ownedGpuTextures.begin(), ownedGpuTextures.end(),
724 [&](const std::unique_ptr<GpuTexture> &g) {
725 return g.get() == gpu;
726 });
727 if (gpuIt == ownedGpuTextures.end()) return false;
728
729 auto texIt = std::find_if(ownedTextures.begin(), ownedTextures.end(),
730 [&](const std::unique_ptr<Texture> &t) {
731 return t.get() == texture;
732 });
733 if (texIt == ownedTextures.end()) return false;
734
735 // Path-cached textures must leave the hot-reload cache once released.
736 for (auto it = texturesByPath.begin(); it != texturesByPath.end();) {
737 if (it->second == texture)
738 it = texturesByPath.erase(it);
739 else
740 ++it;
741 }
742
743 // In-flight frames may still sample the image / sampler; drain first.
744 waitForSharedGpuResources();
745 if ((*gpuIt)->sampler) device->destroySampler((*gpuIt)->sampler);
746 texture->gpuHandle = nullptr;
747 ownedGpuTextures.erase(gpuIt);
748 // Transfer the CPU facade to the caller instead of destroying it.
749 (void)texIt->release();
750 ownedTextures.erase(texIt);
751 return true;
752}
753
754bool Graphics::replaceTexturePixels(Texture *tex, image::ImageData *data) {
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;
761
763 info.sampler = tex->sampler;
764 info.generateMipmaps = tex->mipmapCount > 1;
765 info = normalizeTextureInfo(info);
766 const uint32_t mipLevels =
767 info.generateMipmaps ? uint32_t(mipmapCountForSize(w, h)) : 1u;
768
769 auto gpu = std::make_unique<GpuTexture>();
770 gpu->width = w;
771 gpu->height = h;
772 gpu->isCube = false;
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);
780
781 gpu->sampler = createVkSampler(info.sampler, mipLevels);
782
783 auto sets = vkb::DescriptorSetBuilder().layout(texSetLayout).build(device.instance, descriptorPool);
784
785 gpu->descriptorSet = vkb::BoundSet{sets[0]};
786 writeCombinedImageDescriptor(gpu.get());
787
788 void *oldHandle = tex->gpuHandle;
789 for (auto &owned : ownedGpuTextures) {
790 if (owned.get() != oldHandle) continue;
791 // Destroying the old image/sampler while an in-flight frame still
792 // samples it is a typical TDR. Drain first, then drop cached sets.
793 waitForSharedGpuResources();
794 if (owned->sampler) device->destroySampler(owned->sampler);
795 owned = std::move(gpu);
796 tex->gpuHandle = owned.get();
797 tex->width = w;
798 tex->height = h;
799 tex->pixelWidth = w;
800 tex->pixelHeight = h;
801 tex->mipmapCount = int(mipLevels);
802 tex->sampler = info.sampler;
803 invalidateTextureBindings();
804 return true;
805 }
806
807 // Texture not in owned list — attach as new ownership.
808 tex->gpuHandle = gpu.get();
809 tex->width = w;
810 tex->height = h;
811 tex->pixelWidth = w;
812 tex->pixelHeight = h;
813 tex->mipmapCount = int(mipLevels);
814 tex->sampler = info.sampler;
815 ownedGpuTextures.push_back(std::move(gpu));
816 return true;
817}
818
819Texture *Graphics::newTextureFromFile(const std::string &filename) {
820 ASSERT(!filename.empty());
821 if (filename.empty()) throw Exception("newTextureFromFile: empty filename");
822
823 const std::string key = normalizeTexPath(filename);
824 auto *imgMod = image::Image::create();
825 eve::ref<image::ImageData> data(imgMod->newImageDataFromFile(filename));
826
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());
831 return it->second;
832 }
833
834 Texture *tex = newTexture(data.get());
835 texturesByPath[key] = tex;
836 return tex;
837}
838
839bool Graphics::reloadTextureFromFile(const std::string &filename) {
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;
844
845 image::ImageData *data = nullptr;
846 try {
847 auto *imgMod = image::Image::create();
848 eve::ref<image::ImageData> cached(imgMod->newImageDataFromFile(filename));
849 data = cached.get();
850 } catch (...) {
851 return false;
852 }
853 if (!data) return false;
854 return replaceTexturePixels(it->second, data);
855}
856
857void Graphics::drawTexturedRect(Texture *texture, float x, float y, float w, float h, const Color &color) {
858 drawTexturedRectUV(texture, x, y, w, h, 0.f, 0.f, 1.f, 1.f, color);
859}
860
861void Graphics::drawTexturedRectShader(Texture *texture, Shader *shader, float x, float y, float w,
862 float h, const Color &color) {
863 drawTexturedRectShaderUV(texture, shader, x, y, w, h, 0.f, 0.f, 1.f, 1.f, color);
864}
865
866void Graphics::drawTexturedRectUV(Texture *texture, float x, float y, float w, float h, float u0,
867 float v0, float u1, float v1, const Color &color) {
868 drawTexturedRectShaderUV(texture, currentShader, x, y, w, h, u0, v0, u1, v1, color);
869}
870
871void Graphics::drawTexturedRectShaderUV(Texture *texture, Shader *shader, float x, float y, float w,
872 float h, float u0, float v0, float u1, float v1,
873 const Color &color, bool rotatedUV, BlendMode blend) {
874 if (!texture) {
875 drawSolidRect(x, y, w, h, color, blend);
876 return;
877 }
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{}});
883 }
884 texturedBatches.back().batch.addTexturedRect(x, y, w, h, color, u0, v0, u1, v1, rotatedUV);
885 noteTexturedOverlay(texture);
886}
887
889 float w, float h, float degrees, float u0, float v0,
890 float u1, float v1, const Color &color,
891 bool rotatedUV, BlendMode blend) {
892 if (!texture) {
893 drawSolidRect(cx - w * 0.5f, cy - h * 0.5f, w, h, color, blend);
894 return;
895 }
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{}});
901 }
902 texturedBatches.back().batch.addTexturedRectRotated(cx, cy, w, h, degrees, color, u0, v0, u1, v1,
903 rotatedUV);
904 noteTexturedOverlay(texture);
905}
906
908 float y, float w, float h, const Color &tint) {
909 if (!color) {
910 drawSolidRect(x, y, w, h, tint);
911 return;
912 }
913 if (!depth) {
915 return;
916 }
917 if (texturedBatches.empty() || texturedBatches.back().texture != color ||
918 texturedBatches.back().depth != depth || texturedBatches.back().shader != shader) {
919 texturedBatches.push_back(
920 TexturedBatch{color, depth, shader, BlendMode::Alpha, Batcher{}});
921 }
922 texturedBatches.back().batch.addTexturedRect(x, y, w, h, tint, 0.f, 0.f, 1.f, 1.f);
923 noteTexturedOverlay(color);
924}
925
926void Graphics::setLighting2D(const Lighting2DUBO &ubo) { lighting2dFrame = ubo; }
927
928void Graphics::ensureFlatNormalTexture() {
929 if (flatNormalTexture) return;
930 const uint8_t px[4] = {128, 128, 255, 255}; // flat normal pointing +Z
931 flatNormalTexture = newTexture(1, 1, px);
932}
933
935 float h, float u0, float v0, float u1, float v1,
936 const Color &color) {
937 if (!albedo) {
938 drawSolidRect(x, y, w, h, color);
939 return;
940 }
941 ensureFlatNormalTexture();
942 if (!normal) normal = flatNormalTexture;
943 if (litBatches.empty() || litBatches.back().albedo != albedo ||
944 litBatches.back().normal != normal) {
945 litBatches.push_back(LitBatch{albedo, normal, Batcher{}});
946 }
947 litBatches.back().batch.addTexturedRect(x, y, w, h, color, u0, v0, u1, v1);
948 noteLitOverlay();
949}
950
951vkb::BoundSet Graphics::lit2dSetFor(GpuTexture *albedo, GpuTexture *normal, bool offscreen) {
952 ASSERT(albedo != nullptr);
953 ASSERT(normal != nullptr);
954 auto &sets = offscreen ? offscreenLit2dSets : currentLit2dSets();
955 vkb::GenericBuffer &ubo = offscreen ? offscreenLighting2dUbo : currentLighting2dUbo();
956 LitSetKey key{albedo, normal};
957 auto it = sets.find(key);
958 if (it != sets.end()) return it->second;
959
960 vk::DescriptorSetAllocateInfo alloc{};
961 alloc.descriptorPool = descriptorPool;
962 alloc.descriptorSetCount = 1;
963 alloc.pSetLayouts = &lit2dSetLayout;
964 vkb::UnboundSet unbound{device->allocateDescriptorSets(alloc).front()};
965
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))
974 .update(device.instance);
975
976 vkb::BoundSet bound = std::move(unbound).publish();
977 sets.emplace(key, bound);
978 return bound;
979}
980
981vkb::BoundSet Graphics::post2SetFor(GpuTexture *color, GpuTexture *depth) {
982 if (!color || !color->sampler) return {};
983 vk::ImageView colorView = color->imageView();
984 if (!colorView) return {};
985 if (!depth || !depth->sampler || !depth->imageView()) depth = color;
986 LitSetKey key{color, depth};
987 auto it = post2Sets.find(key);
988 if (it != post2Sets.end()) return it->second;
989
990 auto sets = vkb::DescriptorSetBuilder().layout(texSetLayout).build(device.instance, descriptorPool);
991
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()))
999 .update(device.instance);
1000 vkb::BoundSet bound = std::move(unbound).publish();
1001 post2Sets.emplace(key, bound);
1002 return bound;
1003}
1004
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,
1008 bool offscreen) {
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));
1014
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);
1022
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});
1029
1030 if (texBufIndex >= texBufs.size()) texBufs.emplace_back();
1031 vkb::HostVertexBuffer &vb = texBufs[texBufIndex++];
1032 vb.allocate<TexturedVertex>(frameToken(), device, gpuVerts);
1033
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,
1037 nullptr);
1038 vk::DeviceSize offset = 0;
1039 cb.bindVertexBuffers(0, 1, vb, &offset);
1040 cb.draw(uint32_t(gpuVerts.size()), 1, 0, 0);
1041 }
1042}
1043
1044
1045Shader *Graphics::newShaderFromSpv(const std::vector<uint32_t> &vertSpv,
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");
1050
1051 std::vector<uint32_t> vert = vertSpv;
1052 if (vert.empty())
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");
1056
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);
1064 }
1065
1066 auto sh = std::make_unique<Shader>();
1067 sh->setSpirv(std::move(vert), fragSpv);
1068 sh->gpuHandle = gpu.get();
1069
1070 Shader *raw = sh.get();
1071 ownedShaders.push_back(std::move(sh));
1072 ownedGpuShaders.push_back(std::move(gpu));
1073 return raw;
1074}
1075
1076Shader *Graphics::newShaderFromSpvFile(const std::string &vertPath, const std::string &fragPath) {
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);
1081 return newShaderFromSpv(vert, frag);
1082}
1083
1084Shader *Graphics::newShader(const std::string &vertGlsl, const std::string &fragGlsl) {
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");
1089 return newShaderFromSpv(vert, frag);
1090}
1091
1092Shader *Graphics::newMeshShaderFromSpv(const std::vector<uint32_t> &vertSpv,
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");
1099
1100 std::vector<uint32_t> vert = vertSpv;
1101 if (vert.empty())
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");
1105
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());
1111 // Built here, not lazily in drawMeshShader: vkCreateGraphicsPipelines
1112 // during an open render pass crashes software ICDs (Lavapipe).
1113 gpu->mesh3dXrayPipeline = createMesh3DXrayPipeline(vert, fragSpv, mesh3dShaderPipelineLayout,
1114 activeScenePass(), activeSceneSamples());
1115
1116 auto sh = std::make_unique<Shader>();
1117 sh->setKind(Shader::Kind::eMesh3D);
1118 sh->setSpirv(std::move(vert), fragSpv);
1119 sh->gpuHandle = gpu.get();
1120 gpu->owner = sh.get();
1121
1122 Shader *raw = sh.get();
1123 ownedShaders.push_back(std::move(sh));
1124 ownedGpuShaders.push_back(std::move(gpu));
1125 return raw;
1126}
1127
1128Shader *Graphics::newHairShaderFromSpv(const std::vector<uint32_t> &vertSpv,
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");
1135
1136 std::vector<uint32_t> vert = vertSpv;
1137 if (vert.empty())
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");
1141
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());
1148
1149 auto sh = std::make_unique<Shader>();
1150 sh->setKind(Shader::Kind::eMesh3D);
1151 sh->setSpirv(std::move(vert), fragSpv);
1152 sh->gpuHandle = gpu.get();
1153 gpu->owner = sh.get();
1154
1155 Shader *raw = sh.get();
1156 ownedShaders.push_back(std::move(sh));
1157 ownedGpuShaders.push_back(std::move(gpu));
1158 return raw;
1159}
1160
1162 if (!shader || !shader->gpuHandle) return false;
1163
1164 auto *gpu = static_cast<GpuShader *>(shader->gpuHandle);
1165 auto gpuIt = std::find_if(ownedGpuShaders.begin(), ownedGpuShaders.end(),
1166 [&](const std::unique_ptr<GpuShader> &g) {
1167 return g.get() == gpu;
1168 });
1169 if (gpuIt == ownedGpuShaders.end()) return false;
1170
1171 auto shIt = std::find_if(ownedShaders.begin(), ownedShaders.end(),
1172 [&](const std::unique_ptr<Shader> &s) {
1173 return s.get() == shader;
1174 });
1175 if (shIt == ownedShaders.end()) return false;
1176
1177 // Mirror ~Graphics: pipelines are raw handles that must be destroyed here;
1178 // pipelineLayout is shared and must not be destroyed per-shader.
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);
1186 // Transfer the CPU facade to the caller instead of destroying it.
1187 (void)shIt->release();
1188 ownedShaders.erase(shIt);
1189 return true;
1190}
1191
1192Shader *Graphics::newMeshShader(const std::string &vertGlsl, const std::string &fragGlsl) {
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");
1198}
1199
1200Shader *Graphics::newMeshShaderFromWgsl(const std::string &, const std::string &) {
1201 throw Exception("newMeshShaderFromWgsl: WGSL mesh shaders are only supported on the "
1202 "WebGPU backend; use newMeshShaderFromSpv on Vulkan.");
1203}
1204
1205void Graphics::flushBatch() {
1206 if (!initialized) return;
1207 if (isCanvasActive()) {
1208 auto *oc = dynamic_cast<OffscreenCanvas *>(activeCanvas);
1209 if (!oc) throw Exception("flushBatch: active canvas is not an OffscreenCanvas");
1210 flushToOffscreen(oc);
1211 } else {
1212 flushToSwapchain();
1213 }
1214}
1215
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);
1221 clear2DBatches();
1222
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;
1229
1230 // Offscreen color is a single shared image. An in-flight swapchain frame
1231 // may still be sampling it (draw canvas to screen last frame), so drain
1232 // those frames before transitioning it back to a color attachment.
1233 waitForSharedGpuResources();
1234
1235 vkb::executeImmediately(device.instance, uploadPool, device.getQueue(vkb::QueueType::graphics),
1236 [&](vk::CommandBuffer cb) {
1237 canvas->colorImage().setLayout(cb, vk::ImageLayout::eColorAttachmentOptimal);
1238
1239 vk::ClearValue cv{
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);
1250
1251 setViewportAndScissor(cb, uint32_t(canvas->getWidth()),
1252 uint32_t(canvas->getHeight()));
1253
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();
1260
1261 auto offscreenTexPipe = [&](BlendMode mode) -> vk::Pipeline {
1262 switch (mode) {
1264 return offscreenAdditiveTexPipeline;
1265 case BlendMode::Opaque:
1266 return offscreenOpaqueTexPipeline;
1267 case BlendMode::Alpha:
1268 default:
1269 return offscreenTexPipeline;
1270 }
1271 };
1272 auto offscreenSolidPipe = [&](BlendMode mode) -> vk::Pipeline {
1273 switch (mode) {
1275 return offscreenAdditiveSolidPipeline;
1276 case BlendMode::Alpha:
1277 return offscreenSolidAlphaPipeline;
1278 case BlendMode::Opaque:
1279 default:
1280 return offscreenSolidPipeline;
1281 }
1282 };
1283
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))
1291 texSet = combo;
1292 }
1293 Batcher ndc = tb.batch;
1294 ndc.toNDC(vw, vh);
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});
1299
1300 if (texBufIndex >= texBufs.size()) texBufs.emplace_back();
1301 vkb::HostVertexBuffer &vb = texBufs[texBufIndex++];
1302 vb.allocate<TexturedVertex>(frameToken(), device, gpuVerts);
1303
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());
1318 } else {
1319 vk::Pipeline pipe = offscreenTexPipe(tb.blend);
1320 if (!pipe) return;
1321 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, pipe);
1322 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
1323 texPipelineLayout, 0, 1,
1324 &texSet, 0, nullptr);
1325 }
1326 vk::DeviceSize offset = 0;
1327 cb.bindVertexBuffers(0, 1, vb, &offset);
1328 cb.draw(uint32_t(gpuVerts.size()), 1, 0, 0);
1329 };
1330
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())
1335 return;
1336 vk::Pipeline pipe = offscreenSolidPipe(solid[idx].blend);
1337 if (!pipe) return;
1338 Batcher ndc = solid[idx].batch;
1339 ndc.toNDC(vw, vh);
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,
1346 gpuVerts);
1347 solidUploaded[idx] = true;
1348 };
1349
1350 auto drawSolidSpan = [&](uint32_t batchIndex, uint32_t begin,
1351 uint32_t count) {
1352 if (batchIndex >= solid.size() || count == 0 ||
1353 solid[batchIndex].batch.empty())
1354 return;
1355 vk::Pipeline pipe = offscreenSolidPipe(solid[batchIndex].blend);
1356 if (!pipe) return;
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);
1362 };
1363
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);
1377 }
1378 }
1379 } else {
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()));
1384 }
1385 for (auto &tb : textured) drawOffscreenTextured(tb);
1386 if (offscreenLitPipeline)
1387 drawLitBatches(cb, vw, vh, offscreenLitPipeline, lit, texBufs,
1388 texBufIndex, true);
1389 }
1390
1391 cb.endRenderPass();
1392 canvas->colorImage().endSampledLayout();
1393 });
1394}
1395
1396void Graphics::abortOpen3DFrame() {
1397 const bool hadScene = sceneColorPassOpen;
1398 const bool had3D = swapchainPassOpen;
1399 try {
1400 if (hadScene) endSceneColorRenderPass();
1401 } catch (...) {
1402 sceneColorPassOpen = false;
1403 }
1404 try {
1405 if (had3D) {
1406 // Scene-pass path never opened the swapchain pass; open a dummy
1407 // one so the acquired command buffer can be submitted.
1408 if (hadScene) beginSwapchainColorPass();
1409 presentRecording = swapchainPass.endRenderPass();
1410 swapchainPass = {};
1411 const bool captured = screenReadbackEnabled
1412 ? recordSwapchainReadback(presentRecording.commandBuffer())
1413 : false;
1414 presentRecording.end().submitAndPresent();
1415 presentRecording = {};
1416 if (captured) {
1417 hasPresentedFrame = true;
1418 readbackReady = true;
1419 readbackCpuSynced = false;
1420 }
1421 }
1422 } catch (...) {
1423 swapchainPass = {};
1424 presentRecording = {};
1425 }
1426 swapchainPassOpen = false;
1427 sceneColorPassOpen = false;
1428 frameHad3D = false;
1429 hasPendingClear = false;
1430 flushingSwapchain_ = false;
1431 clear2DBatches();
1432}
1433
1434void Graphics::flushToSwapchain() {
1435 if (flushingSwapchain_) return;
1436 flushingSwapchain_ = true;
1437 bool completed = false;
1438 struct FlushGuard {
1439 Graphics *g;
1440 bool *completed;
1441 ~FlushGuard() {
1442 g->flushingSwapchain_ = false;
1443 if (!*completed) g->abortOpen3DFrame();
1444 }
1445 } guard{this, &completed};
1446
1447 const bool continue3D = swapchainPassOpen;
1448 const bool hadScenePass = sceneColorPassOpen;
1449
1450 if (hadScenePass) {
1451 endSceneColorRenderPass();
1452 queueSceneColorResolve();
1453 }
1454
1455 if (!continue3D) {
1456 // 2D-only path: acquire the present CB and record deferred passes now,
1457 // so the UI overlay's dedicated MSAA pass can be recorded before the
1458 // swapchain pass begins.
1459 if (!beginPresentCommandBuffer()) {
1460 dropPendingOffscreenPasses();
1461 hasPendingClear = false;
1462 completed = true;
1463 return;
1464 }
1465 recordDeferredFrameGraph();
1466 }
1467
1468 // Render the UI overlay (ImGui) into its own MSAA pass, resolved and
1469 // composited as the top-most fullscreen quad. Skipped only on the rare 3D
1470 // fallback path where the swapchain pass is already open from begin3DFrame.
1471 if (presentOverlayFn_ && !(continue3D && !hadScenePass)) {
1472 renderUiOverlayPass();
1473 }
1474
1475 if (hadScenePass || !continue3D) {
1476 beginSwapchainColorPass();
1477 swapchainPassOpen = true;
1478 }
1479
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;
1488 Texture *sceneTex = getSceneColorTexture();
1489 clear2DBatches();
1490
1491 auto &cb = currentPresentCb();
1492 setViewportAndScissor(cb, swapchain.extent.width, swapchain.extent.height);
1493
1494 // Persistent per-frame-slot buffers (see currentFrame2DBuffers). Safe to
1495 // overwrite: acquireForFrame() already waited this slot's fence.
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;
1500
1501 auto swapchainTexPipe = [&](BlendMode mode) -> vk::Pipeline {
1502 switch (mode) {
1504 return additiveTexPipeline;
1505 case BlendMode::Opaque:
1506 return opaqueTexPipeline;
1507 case BlendMode::Alpha:
1508 default:
1509 return texPipeline;
1510 }
1511 };
1512 auto swapchainSolidPipe = [&](BlendMode mode) -> vk::Pipeline {
1513 switch (mode) {
1515 return additiveSolidPipeline;
1516 case BlendMode::Alpha:
1517 return solidAlphaPipeline;
1518 case BlendMode::Opaque:
1519 default:
1520 return pipeline;
1521 }
1522 };
1523
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;
1531 }
1532 Batcher ndc = tb.batch;
1533 ndc.toNDC(width, height);
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});
1538
1539 if (texBufIndex >= texBufs.size()) texBufs.emplace_back();
1540 vkb::HostVertexBuffer &vb = texBufs[texBufIndex++];
1541 vb.allocate<TexturedVertex>(frameToken(), device, gpuVerts);
1542
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,
1550 Shader::kPushConstantBytes, tb.shader->pushConstantData());
1551 } else {
1552 vk::Pipeline pipe = swapchainTexPipe(tb.blend);
1553 if (!pipe) return;
1554 cb.bindPipeline(vk::PipelineBindPoint::eGraphics, pipe);
1555 cb.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, texPipelineLayout, 0, 1,
1556 &texSet, 0, nullptr);
1557 }
1558 vk::DeviceSize offset = 0;
1559 cb.bindVertexBuffers(0, 1, vb, &offset);
1560 cb.draw(uint32_t(gpuVerts.size()), 1, 0, 0);
1561 };
1562
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);
1567 if (!pipe) return;
1568 Batcher ndc = solid[idx].batch;
1569 ndc.toNDC(width, height);
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;
1577 };
1578
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);
1582 if (!pipe) return;
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);
1588 };
1589
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);
1601 }
1602 }
1603 };
1604
1605 bool engineDrawn = false;
1606 auto drawEngine3D = [&]() {
1607 if (engineDrawn) return;
1608 engineDrawn = true;
1609 replaySpans(engineSpans);
1610 };
1611
1612 // Default: blit 3D fullscreen under script 2D. Scripts that call
1613 // drawScene3D / drawTexturedRect(getSceneColorTexture()) own the order.
1614 if (autoScene && sceneResolve) {
1615 drawTextured(*sceneResolve);
1616 drawEngine3D();
1617 }
1618
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()));
1623 }
1624 if (texPipeline) {
1625 for (auto &tb : textured) drawTextured(tb);
1626 }
1627 if (lit2dPipeline) drawLitBatches(cb, width, height, lit2dPipeline, lit, texBufs, texBufIndex,
1628 false);
1629 } else {
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);
1641 }
1642 }
1643 }
1644
1645 if (uiResolve) drawTextured(*uiResolve);
1646
1647 // Invalidate prior readback so a failed present cannot reuse a stale frame.
1648 if (screenReadbackEnabled) hasPresentedFrame = false;
1649
1650 // Fallback path only: the swapchain pass was already open when this frame
1651 // began (3D MSAA scene pass unavailable), so draw the overlay directly.
1652 if (presentOverlayFn_ && continue3D && !hadScenePass) {
1653 VkCommandBuffer raw = static_cast<VkCommandBuffer>(cb);
1655 }
1656
1657 presentRecording = swapchainPass.endRenderPass();
1658 swapchainPass = {};
1659 const bool captured =
1660 screenReadbackEnabled ? recordSwapchainReadback(presentRecording.commandBuffer()) : false;
1661 presentRecording.end().submitAndPresent();
1662 presentRecording = {};
1663 if (captured) {
1664 hasPresentedFrame = true;
1665 readbackReady = true;
1666 readbackCpuSynced = false;
1667 }
1668 hasPendingClear = false;
1669 swapchainPassOpen = false;
1670 frameHad3D = false;
1671 completed = true;
1672}
1673
1674
1675} // namespace eve::graphics::vulkan
float cx
Definition CardTypes.cpp:31
float cy
Definition CardTypes.cpp:32
float degrees
Definition CardTypes.cpp:33
vkb::Device & device
vk::ShaderModule vert
vk::ShaderModule frag
int y
Definition Grass.cpp:135
float height
Definition Grass.cpp:235
float u
Definition Grass.cpp:234
int x
Definition Grass.cpp:135
glm::vec3 n
Definition Grass.cpp:64
int h
int w
std::vector< Colorf > px
float depth
uint32_t a
uint32_t b
uint32_t c
float tb
Texture * normal
Texture * albedo
int width
int idx
glm::mat4 viewProj
Shader * shader
glm::mat4 view
Light2D::Data * data
Color clearColor
const char * name
Definition RockMesh.cpp:21
bool repeatV
bool repeatU
int d
int v
image::ImageData::Colorf color
std::string image
float m[16]
uint32_t s
Definition Weather.cpp:28
Accumulates solid / textured quads in logical (Y-down) coordinates. Used by RenderSystem; not a publi...
Definition Batcher.h:22
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,...
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.
Definition Graphics.h:200
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),...
Definition Graphics.h:461
bool recordingEngine3D_
True while RenderSystem3D is submitting (AO / engine overlays).
Definition Graphics.h:980
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.
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:768
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...
Definition Graphics.h:432
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_
Definition Graphics.h:985
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.
Custom GPU program.
Definition Shader.h:30
static constexpr uint32_t kPushConstantBytes
Definition Shader.h:33
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:17
TextureSampler sampler
Definition Texture.h:43
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.
Definition ImageData.h:26
ref is a smart pointer that automatically calls ref() and unref() on the object. Type T must be a sub...
Definition Object.h:54
T * get()
Definition Object.h:94
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...
Definition Color.h:13
BlendMode
2D quad blend mode (drawn in draw order within a layer).
Definition BlendMode.h:6
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:457
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".