MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
room.cpp
Go to the documentation of this file.
1#include <SDL3/SDL.h>
2#include <algorithm>
3#include <array>
4#include <charconv>
5#include <chrono>
6#include <cmath>
7#include <cstdint>
8#include <cstdlib>
9#include <cstring>
10#include <filesystem>
11#include <format>
12#include <glm/ext/matrix_clip_space.hpp>
13#include <glm/ext/matrix_transform.hpp>
14#include <glm/glm.hpp>
15#include <iostream>
16#include <random>
17#include <string>
18#include <string_view>
19#include <vector>
20
21#include "mxvk/argz.hpp"
22#include "mxvk/mxvk.hpp"
26#include "mxvk/mxvk_png.hpp"
28
29namespace walk {
30
31 struct WallSegment {
32 glm::vec3 start{0.0f};
33 glm::vec3 end{0.0f};
34 float height = 5.0f;
35 };
36
38 glm::vec3 position{0.0f};
39 float radius = 1.0f;
40 float height = 4.0f;
41 };
42
43 struct Collectible {
44 enum class Type {
47 };
48
50 glm::vec3 position{0.0f};
51 glm::vec3 hitCenterOffset{0.0f};
52 glm::vec3 rotation{0.0f};
53 glm::vec3 scale{1.0f};
54 float rotationSpeed = 12.0f;
55 float radius = 1.0f;
56 bool active = true;
57 };
58
59 struct Projectile {
60 struct TrailPoint {
61 glm::vec3 position{0.0f};
62 float lifetime = 0.0f;
63 float maxLifetime = 0.5f;
64 };
65
66 glm::vec3 position{0.0f};
67 glm::vec3 direction{0.0f, 0.0f, -1.0f};
68 float speed = 100.0f;
69 float lifetime = 0.0f;
70 float maxLifetime = 10.0f;
71 float distanceTraveled = 0.0f;
72 float maxDistance = 160.0f;
73 bool active = true;
74 std::vector<TrailPoint> trail{};
75 float trailTimer = 0.0f;
76 };
77
79 glm::vec3 position{0.0f};
80 glm::vec3 velocity{0.0f};
81 glm::vec3 color{1.0f, 0.5f, 0.2f};
82 float lifetime = 0.0f;
83 float maxLifetime = 0.55f;
84 float size = 0.08f;
85 bool active = true;
86 };
87
89 glm::vec3 pos{0.0f};
90 glm::vec4 color{1.0f};
91 float size = 8.0f;
92 };
93
94 class MazeWorld {
95 public:
96 [[nodiscard]] glm::vec3 startPosition() const noexcept { return startPositionValue; }
97
98 [[nodiscard]] const std::vector<WallSegment> &walls() const noexcept { return wallSegments; }
99
100 [[nodiscard]] const std::vector<PillarInstance> &pillars() const noexcept { return pillarInstances; }
101
102 [[nodiscard]] std::vector<Collectible> &collectibles() noexcept { return collectibleItems; }
103
104 [[nodiscard]] const std::vector<Collectible> &collectibles() const noexcept { return collectibleItems; }
105
106 [[nodiscard]] int activeCollectibles() const {
107 int count = 0;
108 for (const Collectible &obj : collectibleItems) {
109 if (obj.active) {
110 ++count;
111 }
112 }
113 return count;
114 }
115
116 void generate(uint32_t seed) {
117 std::mt19937 rng(seed);
118 generateMaze(rng);
119 generatePillars(rng);
120 // Rescue: if the player start happens to land inside a pillar, push the
121 // start point to the safest unoccupied spot in the same cell.
122 if (checkPillarCollision(startPositionValue, 0.6f)) {
123 for (int attempt = 0; attempt < 64; ++attempt) {
124 const glm::vec3 candidate = randomPointInCell(startCellX, startCellZ, 0.6f, eyeHeight, rng, 0.5f);
125 if (!checkWallCollision(candidate, 0.5f) && !checkPillarCollision(candidate, 0.6f)) {
126 startPositionValue = candidate;
127 break;
128 }
129 }
130 }
131 generateCollectibles(rng);
132 }
133
134 [[nodiscard]] bool checkWallCollision(const glm::vec3 &position, float radius) const {
135 const float halfThickness = wallThicknessValue * 0.5f;
136 const float hitRadius = radius + halfThickness;
137 for (const WallSegment &wall : wallSegments) {
138 const glm::vec3 segment = wall.end - wall.start;
139 const float segmentLength = glm::length(segment);
140 if (segmentLength < 0.0001f) {
141 continue;
142 }
143
144 const glm::vec3 segmentDir = segment / segmentLength;
145 const glm::vec3 toPoint = position - wall.start;
146 const float projection = glm::clamp(glm::dot(toPoint, segmentDir), 0.0f, segmentLength);
147 glm::vec3 closest = wall.start + (segmentDir * projection);
148 closest.y = position.y;
149 if (glm::length(position - closest) < hitRadius) {
150 return true;
151 }
152 }
153 return false;
154 }
155
156 [[nodiscard]] bool checkPillarCollision(const glm::vec3 &position, float playerRadius) const {
157 for (const PillarInstance &pillar : pillarInstances) {
158 const glm::vec2 player2d(position.x, position.z);
159 const glm::vec2 pillar2d(pillar.position.x, pillar.position.z);
160 if (glm::length(player2d - pillar2d) < (playerRadius + pillar.radius)) {
161 return true;
162 }
163 }
164 return false;
165 }
166
167 [[nodiscard]] bool checkCollectibleCollision(const glm::vec3 &point, size_t &indexOut) const {
168 for (size_t i = 0; i < collectibleItems.size(); ++i) {
169 if (!collectibleItems[i].active) {
170 continue;
171 }
172 const Collectible &collectible = collectibleItems[i];
173 const glm::vec3 center = collectible.position + collectible.hitCenterOffset;
174 if (collectible.type == Collectible::Type::Bird) {
175 const glm::vec3 delta = point - center;
176 const float halfSide = collectible.radius;
177 if (std::abs(delta.x) <= halfSide &&
178 std::abs(delta.y) <= halfSide &&
179 std::abs(delta.z) <= halfSide) {
180 indexOut = i;
181 return true;
182 }
183 continue;
184 }
185
186 if (glm::length(center - point) < collectible.radius) {
187 indexOut = i;
188 return true;
189 }
190 }
191 return false;
192 }
193
194 [[nodiscard]] glm::vec3 randomPointInCell(int cellX, int cellZ, float objectRadius, float y, std::mt19937 &rng, float margin) const {
195 const float pad = objectRadius + wallThicknessValue * 0.5f + margin;
196 const float x0 = -size + static_cast<float>(cellX) * cellSize;
197 const float z0 = -size + static_cast<float>(cellZ) * cellSize;
198 const float x1 = x0 + cellSize;
199 const float z1 = z0 + cellSize;
200
201 float minX = x0 + pad;
202 float maxX = x1 - pad;
203 float minZ = z0 + pad;
204 float maxZ = z1 - pad;
205 if (minX > maxX) {
206 minX = maxX = (x0 + x1) * 0.5f;
207 }
208 if (minZ > maxZ) {
209 minZ = maxZ = (z0 + z1) * 0.5f;
210 }
211
212 std::uniform_real_distribution<float> distX(minX, maxX);
213 std::uniform_real_distribution<float> distZ(minZ, maxZ);
214 return glm::vec3(distX(rng), y, distZ(rng));
215 }
216
217 [[nodiscard]] float wallThickness() const noexcept { return wallThicknessValue; }
218
219 private:
220 struct Cell {
221 bool visited = false;
222 std::array<bool, 4> walls{true, true, true, true};
223 };
224
225 void generateMaze(std::mt19937 &rng) {
226 const int gridX = mazeGridX;
227 const int gridZ = mazeGridZ;
228 cellSize = (size * 2.0f) / static_cast<float>(gridX);
229
230 auto indexFor = [gridX](int x, int z) {
231 return z * gridX + x;
232 };
233
234 std::vector<Cell> grid(static_cast<size_t>(gridX * gridZ));
235 std::vector<std::pair<int, int>> stack;
236 stack.emplace_back(0, 0);
237 grid[static_cast<size_t>(indexFor(0, 0))].visited = true;
238
239 while (!stack.empty()) {
240 const auto [x, z] = stack.back();
241 std::vector<int> dirs;
242 if (z > 0 && !grid[static_cast<size_t>(indexFor(x, z - 1))].visited) {
243 dirs.push_back(0);
244 }
245 if (x < (gridX - 1) && !grid[static_cast<size_t>(indexFor(x + 1, z))].visited) {
246 dirs.push_back(1);
247 }
248 if (z < (gridZ - 1) && !grid[static_cast<size_t>(indexFor(x, z + 1))].visited) {
249 dirs.push_back(2);
250 }
251 if (x > 0 && !grid[static_cast<size_t>(indexFor(x - 1, z))].visited) {
252 dirs.push_back(3);
253 }
254
255 if (dirs.empty()) {
256 stack.pop_back();
257 continue;
258 }
259
260 std::uniform_int_distribution<size_t> pick(0, dirs.size() - 1U);
261 const int d = dirs[pick(rng)];
262 int nx = x;
263 int nz = z;
264 if (d == 0) {
265 nz = z - 1;
266 } else if (d == 1) {
267 nx = x + 1;
268 } else if (d == 2) {
269 nz = z + 1;
270 } else {
271 nx = x - 1;
272 }
273
274 grid[static_cast<size_t>(indexFor(x, z))].walls[static_cast<size_t>(d)] = false;
275 grid[static_cast<size_t>(indexFor(nx, nz))].walls[static_cast<size_t>((d + 2) % 4)] = false;
276 grid[static_cast<size_t>(indexFor(nx, nz))].visited = true;
277 stack.emplace_back(nx, nz);
278 }
279
280 wallSegments.clear();
281 for (int z = 0; z < gridZ; ++z) {
282 for (int x = 0; x < gridX; ++x) {
283 const float cx = -size + static_cast<float>(x) * cellSize;
284 const float cz = -size + static_cast<float>(z) * cellSize;
285 const float x0 = cx;
286 const float z0 = cz;
287 const float x1 = cx + cellSize;
288 const float z1 = cz + cellSize;
289 const Cell &cell = grid[static_cast<size_t>(indexFor(x, z))];
290
291 if (cell.walls[0]) {
292 wallSegments.push_back({glm::vec3(x0, 0.0f, z0), glm::vec3(x1, 0.0f, z0), wallHeight});
293 }
294 if (cell.walls[3]) {
295 wallSegments.push_back({glm::vec3(x0, 0.0f, z1), glm::vec3(x0, 0.0f, z0), wallHeight});
296 }
297 if (x == (gridX - 1) && cell.walls[1]) {
298 wallSegments.push_back({glm::vec3(x1, 0.0f, z0), glm::vec3(x1, 0.0f, z1), wallHeight});
299 }
300 if (z == (gridZ - 1) && cell.walls[2]) {
301 wallSegments.push_back({glm::vec3(x1, 0.0f, z1), glm::vec3(x0, 0.0f, z1), wallHeight});
302 }
303 }
304 }
305 mergeContiguousWalls();
306
307 const float playerRadius = 0.5f;
308 startCellX = 0;
309 startCellZ = 0;
310 startPositionValue = randomPointInCell(0, 0, playerRadius, eyeHeight, rng, 0.5f);
311 if (checkWallCollision(startPositionValue, playerRadius)) {
312 for (int z = 0; z < mazeGridZ; ++z) {
313 for (int x = 0; x < mazeGridX; ++x) {
314 startPositionValue = randomPointInCell(x, z, playerRadius, eyeHeight, rng, 0.5f);
315 if (!checkWallCollision(startPositionValue, playerRadius)) {
316 startCellX = x;
317 startCellZ = z;
318 return;
319 }
320 }
321 }
322 }
323 }
324
325 void mergeContiguousWalls() {
326 if (wallSegments.empty()) {
327 return;
328 }
329
330 struct NormalizedWall {
331 bool horizontal = false;
332 float constantAxis = 0.0f;
333 float startAxis = 0.0f;
334 float endAxis = 0.0f;
335 float height = 0.0f;
336 };
337
338 constexpr float epsilon = 0.0001f;
339 constexpr float adjacencyEpsilon = 0.001f;
340 constexpr float quantizeScale = 1000.0f;
341
342 const auto quantize = [](float value) {
343 return static_cast<int>(std::lround(value * quantizeScale));
344 };
345
346 std::vector<NormalizedWall> normalized;
347 normalized.reserve(wallSegments.size());
348 for (const WallSegment &wall : wallSegments) {
349 const bool horizontal = std::abs(wall.start.z - wall.end.z) <= epsilon;
350 if (horizontal) {
351 const float x0 = std::min(wall.start.x, wall.end.x);
352 const float x1 = std::max(wall.start.x, wall.end.x);
353 normalized.push_back({true, wall.start.z, x0, x1, wall.height});
354 } else {
355 const float z0 = std::min(wall.start.z, wall.end.z);
356 const float z1 = std::max(wall.start.z, wall.end.z);
357 normalized.push_back({false, wall.start.x, z0, z1, wall.height});
358 }
359 }
360
361 std::sort(normalized.begin(), normalized.end(), [&quantize](const NormalizedWall &a, const NormalizedWall &b) {
362 const auto keyA = std::array<int, 3>{a.horizontal ? 1 : 0, quantize(a.constantAxis), quantize(a.height)};
363 const auto keyB = std::array<int, 3>{b.horizontal ? 1 : 0, quantize(b.constantAxis), quantize(b.height)};
364 if (keyA != keyB) {
365 return keyA < keyB;
366 }
367 if (a.startAxis != b.startAxis) {
368 return a.startAxis < b.startAxis;
369 }
370 return a.endAxis < b.endAxis;
371 });
372
373 std::vector<WallSegment> merged;
374 merged.reserve(normalized.size());
375 size_t index = 0;
376 while (index < normalized.size()) {
377 const NormalizedWall first = normalized[index];
378 float runStart = first.startAxis;
379 float runEnd = first.endAxis;
380
381 size_t next = index + 1;
382 while (next < normalized.size()) {
383 const NormalizedWall &candidate = normalized[next];
384 if (candidate.horizontal != first.horizontal || quantize(candidate.constantAxis) != quantize(first.constantAxis) || quantize(candidate.height) != quantize(first.height)) {
385 break;
386 }
387
388 if (candidate.startAxis <= (runEnd + adjacencyEpsilon)) {
389 runEnd = std::max(runEnd, candidate.endAxis);
390 ++next;
391 continue;
392 }
393
394 if (first.horizontal) {
395 merged.push_back({glm::vec3(runStart, 0.0f, first.constantAxis), glm::vec3(runEnd, 0.0f, first.constantAxis), first.height});
396 } else {
397 merged.push_back({glm::vec3(first.constantAxis, 0.0f, runStart), glm::vec3(first.constantAxis, 0.0f, runEnd), first.height});
398 }
399 runStart = candidate.startAxis;
400 runEnd = candidate.endAxis;
401 ++next;
402 }
403
404 if (first.horizontal) {
405 merged.push_back({glm::vec3(runStart, 0.0f, first.constantAxis), glm::vec3(runEnd, 0.0f, first.constantAxis), first.height});
406 } else {
407 merged.push_back({glm::vec3(first.constantAxis, 0.0f, runStart), glm::vec3(first.constantAxis, 0.0f, runEnd), first.height});
408 }
409 index = next;
410 }
411
412 wallSegments.swap(merged);
413 }
414
415 void generatePillars(std::mt19937 &rng) {
416 pillarInstances.clear();
417 std::uniform_real_distribution<float> radiusDist(0.5f, 1.5f);
418 std::uniform_real_distribution<float> heightDist(3.0f, 6.0f);
419
420 constexpr int targetPillars = 15;
421 constexpr int maxAttempts = targetPillars * 8;
422 int created = 0;
423 for (int attempt = 0; attempt < maxAttempts && created < targetPillars; ++attempt) {
424 const int cellX = static_cast<int>(rng() % static_cast<uint32_t>(mazeGridX));
425 const int cellZ = static_cast<int>(rng() % static_cast<uint32_t>(mazeGridZ));
426 // Don't drop pillars on top of where the player spawns.
427 if (cellX == startCellX && cellZ == startCellZ) {
428 continue;
429 }
430 PillarInstance pillar{};
431 pillar.radius = radiusDist(rng);
432 pillar.height = heightDist(rng);
433 pillar.position = randomPointInCell(cellX, cellZ, pillar.radius, 0.0f, rng, 0.3f);
434 if (!checkWallCollision(pillar.position, pillar.radius)) {
435 pillarInstances.push_back(pillar);
436 ++created;
437 }
438 }
439 }
440
441 void generateCollectibles(std::mt19937 &rng) {
442 collectibleItems.clear();
443 const int usableCells = std::max(1, (mazeGridX * mazeGridZ) - 1);
444 const int targetCollectibles = usableCells * collectiblesPerCell;
445 collectibleItems.reserve(static_cast<size_t>(targetCollectibles));
446
447 std::vector<Collectible::Type> types;
448 types.reserve(static_cast<size_t>(targetCollectibles));
449 const int saturnCount = targetCollectibles / 2;
450 for (int i = 0; i < saturnCount; ++i) {
451 types.push_back(Collectible::Type::Saturn);
452 }
453 for (int i = saturnCount; i < targetCollectibles; ++i) {
454 types.push_back(Collectible::Type::Bird);
455 }
456 std::shuffle(types.begin(), types.end(), rng);
457
458 std::uniform_real_distribution<float> saturnScale(0.4f, 0.8f);
459 std::uniform_real_distribution<float> saturnRotSpeed(5.0f, 15.0f);
460 std::uniform_real_distribution<float> birdScale(0.3f, 0.5f);
461 std::uniform_real_distribution<float> birdRotSpeed(20.0f, 60.0f);
462
463 int typeIndex = 0;
464 for (int cellZ = 0; cellZ < mazeGridZ; ++cellZ) {
465 for (int cellX = 0; cellX < mazeGridX; ++cellX) {
466 if (cellX == startCellX && cellZ == startCellZ) {
467 continue;
468 }
469 for (int slot = 0; slot < collectiblesPerCell; ++slot) {
470 if (typeIndex >= targetCollectibles) {
471 break;
472 }
473 Collectible obj{};
474 obj.type = types[static_cast<size_t>(typeIndex)];
475 if (obj.type == Collectible::Type::Saturn) {
476 const float scale = saturnScale(rng);
477 obj.scale = glm::vec3(scale);
478 obj.rotationSpeed = saturnRotSpeed(rng);
479 obj.radius = 2.0f * scale;
480 } else {
481 const float scale = birdScale(rng);
482 obj.scale = glm::vec3(scale);
483 obj.rotationSpeed = birdRotSpeed(rng);
484 obj.radius = 0.5f * scale;
485 }
486
487 bool foundSpot = false;
488 glm::vec3 fallback = glm::vec3(0.0f, (obj.type == Collectible::Type::Bird) ? obj.radius : 2.5f, 0.0f);
489 for (int attempt = 0; attempt < 24 && !foundSpot; ++attempt) {
490 const float y = (obj.type == Collectible::Type::Bird) ? obj.radius : 2.5f;
491 const float margin = (attempt < 18) ? 0.45f : 0.10f;
492 const glm::vec3 candidate = randomPointInCell(cellX, cellZ, obj.radius, y, rng, margin);
493 fallback = candidate;
494
495 bool overlapsOtherCollectible = false;
496 for (const Collectible &placed : collectibleItems) {
497 const float separation = std::max(5.0f, placed.radius + obj.radius + 0.2f);
498 if (glm::length(placed.position - candidate) < separation) {
499 overlapsOtherCollectible = true;
500 break;
501 }
502 }
503
504 if (!overlapsOtherCollectible && !checkWallCollision(candidate, obj.radius) && !checkPillarCollision(candidate, obj.radius)) {
505 obj.position = candidate;
506 foundSpot = true;
507 }
508 }
509
510 if (!foundSpot) {
511 // Keep spawn count fixed: use the last in-cell candidate as a fallback.
512 obj.position = fallback;
513 }
514 collectibleItems.push_back(obj);
515 ++typeIndex;
516 }
517 }
518 }
519 }
520
521 std::vector<WallSegment> wallSegments{};
522 std::vector<PillarInstance> pillarInstances{};
523 std::vector<Collectible> collectibleItems{};
524
525 float size = 50.0f;
526 float wallHeight = 5.0f;
527 float wallThicknessValue = 0.5f;
528 int mazeGridX = 6;
529 int mazeGridZ = 6;
530 int collectiblesPerCell = 1;
531 float cellSize = 0.0f;
532 float eyeHeight = 1.7f;
533 int startCellX = 0;
534 int startCellZ = 0;
535 glm::vec3 startPositionValue{0.0f, 1.7f, 0.0f};
536 };
537
539 public:
541 glm::vec3 position{0.0f};
542 glm::vec2 texCoord{0.0f};
543 glm::vec3 normal{0.0f};
544 };
545
547 glm::mat4 view{1.0f};
548 glm::mat4 proj{1.0f};
549 glm::vec4 fx{0.0f};
550 };
551
552 void load(mxvk::VK_Window *targetWindow,
553 const std::string &textureManifestPath,
554 const std::string &textureBasePath,
555 const std::vector<char> &vertSpv,
556 const std::vector<char> &fragSpv) {
557 if (targetWindow == nullptr) {
558 throw mxvk::Exception("walk: raw pillar renderer requires a valid window");
559 }
560 window = targetWindow;
561 vertexSpv = vertSpv;
562 fragmentSpv = fragSpv;
563
564 if (!window->ensureRenderResources()) {
565 throw mxvk::Exception("walk: raw pillar renderer requires render resources");
566 }
567
568 buildGeometry();
569 loadTexture(textureManifestPath, textureBasePath);
570 createTextureSampler();
571 createDescriptorSetLayout();
572 createUniformBuffers();
573 createDescriptorPool();
574 createDescriptorSets();
575 createPipeline();
576 }
577
578 void resize(mxvk::VK_Window *targetWindow) {
579 if (targetWindow == nullptr || targetWindow->getDevice() == VK_NULL_HANDLE) {
580 return;
581 }
582
583 window = targetWindow;
584 destroyPipeline();
585 destroyDescriptors();
586 createDescriptorSetLayout();
587 createUniformBuffers();
588 createDescriptorPool();
589 createDescriptorSets();
590 createPipeline();
591 }
592
593 /// @brief Hot-swap the fragment shader without rebuilding geometry or descriptors.
594 /// @param newFragSpv Compiled SPIR-V bytecode for the new fragment shader.
595 void reloadFragShader(const std::vector<char> &newFragSpv) {
596 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE || newFragSpv.empty()) {
597 return;
598 }
599 vkDeviceWaitIdle(window->getDevice());
600 fragmentSpv = newFragSpv;
601 destroyPipeline();
602 createPipeline();
603 }
604
605 void cleanup(mxvk::VK_Window *targetWindow) {
606 if (targetWindow == nullptr || targetWindow->getDevice() == VK_NULL_HANDLE) {
607 return;
608 }
609
610 window = targetWindow;
611 destroyPipeline();
612 destroyDescriptors();
613 destroyTexture();
614 destroyBuffers();
615 window = nullptr;
616 }
617
618 void render(VkCommandBuffer cmd,
619 uint32_t imageIndex,
620 const std::vector<PillarInstance> &pillars,
621 const glm::mat4 &view,
622 const glm::mat4 &proj,
623 const glm::vec4 &fx) {
624 if (cmd == VK_NULL_HANDLE || pipeline == VK_NULL_HANDLE || pipelineLayout == VK_NULL_HANDLE) {
625 return;
626 }
627 if (imageIndex >= uniformBuffersMapped.size() || descriptorSets.empty() || vertexBuffer == VK_NULL_HANDLE || indexBuffer == VK_NULL_HANDLE) {
628 return;
629 }
630
631 PillarUniforms uniforms{};
632 uniforms.view = view;
633 uniforms.proj = proj;
634 uniforms.fx = fx;
635 std::memcpy(uniformBuffersMapped[imageIndex], &uniforms, sizeof(PillarUniforms));
636
637 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
638 vkCmdBindDescriptorSets(cmd,
639 VK_PIPELINE_BIND_POINT_GRAPHICS,
640 pipelineLayout,
641 0,
642 1,
643 &descriptorSets[imageIndex],
644 0,
645 nullptr);
646
647 const VkDeviceSize offset = 0;
648 vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset);
649 vkCmdBindIndexBuffer(cmd, indexBuffer, 0, VK_INDEX_TYPE_UINT32);
650
651 for (const PillarInstance &pillar : pillars) {
652 // Vertex data is defined with Y in [0..1] (base at 0.0, top at 1.0).
653 // To avoid z-fighting with the floor, sink the base slightly into the floor
654 // and place the translation at the pillar base Y.
655 constexpr float baseSink = 0.02f;
656 glm::mat4 model = glm::translate(glm::mat4(1.0f), glm::vec3(pillar.position.x, pillar.position.y - baseSink, pillar.position.z));
657 model = glm::scale(model, glm::vec3(pillar.radius, pillar.height, pillar.radius));
658 vkCmdPushConstants(cmd, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(glm::mat4), &model);
659 vkCmdDrawIndexed(cmd, indexCount, 1, 0, 0, 0);
660 }
661 }
662
663 private:
664 [[nodiscard]] uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) const {
665 VkPhysicalDeviceMemoryProperties memProperties{};
666 vkGetPhysicalDeviceMemoryProperties(window->getPhysicalDevice(), &memProperties);
667 for (uint32_t i = 0; i < memProperties.memoryTypeCount; ++i) {
668 if ((typeFilter & (1u << i)) != 0u && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
669 return i;
670 }
671 }
672 throw mxvk::Exception("walk: failed to find suitable memory type for raw pillar renderer");
673 }
674
675 void createBuffer(VkDeviceSize size,
676 VkBufferUsageFlags usage,
677 VkMemoryPropertyFlags properties,
678 VkBuffer &buffer,
679 VkDeviceMemory &bufferMemory) const {
680 VkBufferCreateInfo bufferInfo{};
681 bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
682 bufferInfo.size = size;
683 bufferInfo.usage = usage;
684 bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
685
686 if (vkCreateBuffer(window->getDevice(), &bufferInfo, nullptr, &buffer) != VK_SUCCESS) {
687 throw mxvk::Exception("walk: failed to create raw pillar buffer");
688 }
689
690 VkMemoryRequirements requirements{};
691 vkGetBufferMemoryRequirements(window->getDevice(), buffer, &requirements);
692
693 VkMemoryAllocateInfo allocInfo{};
694 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
695 allocInfo.allocationSize = requirements.size;
696
697 try {
698 allocInfo.memoryTypeIndex = findMemoryType(requirements.memoryTypeBits, properties);
699 if (vkAllocateMemory(window->getDevice(), &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) {
700 throw mxvk::Exception("walk: failed to allocate raw pillar buffer memory");
701 }
702
703 if (vkBindBufferMemory(window->getDevice(), buffer, bufferMemory, 0) != VK_SUCCESS) {
704 throw mxvk::Exception("walk: failed to bind raw pillar buffer memory");
705 }
706 } catch (...) {
707 if (bufferMemory != VK_NULL_HANDLE) {
708 vkFreeMemory(window->getDevice(), bufferMemory, nullptr);
709 bufferMemory = VK_NULL_HANDLE;
710 }
711 if (buffer != VK_NULL_HANDLE) {
712 vkDestroyBuffer(window->getDevice(), buffer, nullptr);
713 buffer = VK_NULL_HANDLE;
714 }
715 throw;
716 }
717 }
718
719 void createImage(uint32_t width,
720 uint32_t height,
721 VkFormat format,
722 VkImageTiling tiling,
723 VkImageUsageFlags usage,
724 VkMemoryPropertyFlags properties,
725 VkImage &image,
726 VkDeviceMemory &memory) const {
727 VkImageCreateInfo imageInfo{};
728 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
729 imageInfo.imageType = VK_IMAGE_TYPE_2D;
730 imageInfo.extent.width = width;
731 imageInfo.extent.height = height;
732 imageInfo.extent.depth = 1;
733 imageInfo.mipLevels = 1;
734 imageInfo.arrayLayers = 1;
735 imageInfo.format = format;
736 imageInfo.tiling = tiling;
737 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
738 imageInfo.usage = usage;
739 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
740 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
741
742 if (vkCreateImage(window->getDevice(), &imageInfo, nullptr, &image) != VK_SUCCESS) {
743 throw mxvk::Exception("walk: failed to create raw pillar image");
744 }
745
746 VkMemoryRequirements requirements{};
747 vkGetImageMemoryRequirements(window->getDevice(), image, &requirements);
748
749 VkMemoryAllocateInfo allocInfo{};
750 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
751 allocInfo.allocationSize = requirements.size;
752
753 try {
754 allocInfo.memoryTypeIndex = findMemoryType(requirements.memoryTypeBits, properties);
755 if (vkAllocateMemory(window->getDevice(), &allocInfo, nullptr, &memory) != VK_SUCCESS) {
756 throw mxvk::Exception("walk: failed to allocate raw pillar image memory");
757 }
758
759 if (vkBindImageMemory(window->getDevice(), image, memory, 0) != VK_SUCCESS) {
760 throw mxvk::Exception("walk: failed to bind raw pillar image memory");
761 }
762 } catch (...) {
763 if (memory != VK_NULL_HANDLE) {
764 vkFreeMemory(window->getDevice(), memory, nullptr);
765 memory = VK_NULL_HANDLE;
766 }
767 if (image != VK_NULL_HANDLE) {
768 vkDestroyImage(window->getDevice(), image, nullptr);
769 image = VK_NULL_HANDLE;
770 }
771 throw;
772 }
773 }
774
775 VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags) const {
776 VkImageViewCreateInfo viewInfo{};
777 viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
778 viewInfo.image = image;
779 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
780 viewInfo.format = format;
781 viewInfo.subresourceRange.aspectMask = aspectFlags;
782 viewInfo.subresourceRange.baseMipLevel = 0;
783 viewInfo.subresourceRange.levelCount = 1;
784 viewInfo.subresourceRange.baseArrayLayer = 0;
785 viewInfo.subresourceRange.layerCount = 1;
786
787 VkImageView imageView = VK_NULL_HANDLE;
788 if (vkCreateImageView(window->getDevice(), &viewInfo, nullptr, &imageView) != VK_SUCCESS) {
789 throw mxvk::Exception("walk: failed to create raw pillar image view");
790 }
791 return imageView;
792 }
793
794 [[nodiscard]] VkCommandBuffer beginSingleTimeCommands() const {
795 VkCommandBufferAllocateInfo allocInfo{};
796 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
797 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
798 allocInfo.commandPool = window->getCommandPool();
799 allocInfo.commandBufferCount = 1;
800
801 VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
802 if (vkAllocateCommandBuffers(window->getDevice(), &allocInfo, &commandBuffer) != VK_SUCCESS) {
803 throw mxvk::Exception("walk: failed to allocate raw pillar command buffer");
804 }
805
806 VkCommandBufferBeginInfo beginInfo{};
807 beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
808 beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
809 if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) {
810 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
811 throw mxvk::Exception("walk: failed to begin raw pillar command buffer");
812 }
813
814 return commandBuffer;
815 }
816
817 void endSingleTimeCommands(VkCommandBuffer commandBuffer) const {
818 if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
819 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
820 throw mxvk::Exception("walk: failed to end raw pillar command buffer");
821 }
822
823 VkSubmitInfo submitInfo{};
824 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
825 submitInfo.commandBufferCount = 1;
826 submitInfo.pCommandBuffers = &commandBuffer;
827
828 if (vkQueueSubmit(window->getGraphicsQueue(), 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
829 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
830 throw mxvk::Exception("walk: failed to submit raw pillar command buffer");
831 }
832 if (vkQueueWaitIdle(window->getGraphicsQueue()) != VK_SUCCESS) {
833 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
834 throw mxvk::Exception("walk: failed to wait for raw pillar upload queue");
835 }
836
837 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
838 }
839
840 void transitionImageLayout(VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout) const {
841 VkCommandBuffer cmd = beginSingleTimeCommands();
842
843 VkImageMemoryBarrier barrier{};
844 barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
845 barrier.oldLayout = oldLayout;
846 barrier.newLayout = newLayout;
847 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
848 barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
849 barrier.image = image;
850 barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
851 barrier.subresourceRange.baseMipLevel = 0;
852 barrier.subresourceRange.levelCount = 1;
853 barrier.subresourceRange.baseArrayLayer = 0;
854 barrier.subresourceRange.layerCount = 1;
855
856 VkPipelineStageFlags sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
857 VkPipelineStageFlags destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
858 if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
859 barrier.srcAccessMask = 0;
860 barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
861 } else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
862 barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
863 barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
864 sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
865 destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
866 }
867
868 vkCmdPipelineBarrier(cmd, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
869 endSingleTimeCommands(cmd);
870 }
871
872 void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) const {
873 VkCommandBuffer cmd = beginSingleTimeCommands();
874 VkBufferImageCopy region{};
875 region.bufferOffset = 0;
876 region.bufferRowLength = 0;
877 region.bufferImageHeight = 0;
878 region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
879 region.imageSubresource.mipLevel = 0;
880 region.imageSubresource.baseArrayLayer = 0;
881 region.imageSubresource.layerCount = 1;
882 region.imageOffset = {0, 0, 0};
883 region.imageExtent = {width, height, 1};
884
885 vkCmdCopyBufferToImage(cmd, buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
886 endSingleTimeCommands(cmd);
887 }
888
889 void createTextureSampler() {
890 if (textureSampler != VK_NULL_HANDLE) {
891 return;
892 }
893
894 VkPhysicalDeviceFeatures deviceFeatures{};
895 vkGetPhysicalDeviceFeatures(window->getPhysicalDevice(), &deviceFeatures);
896 VkPhysicalDeviceProperties deviceProperties{};
897 vkGetPhysicalDeviceProperties(window->getPhysicalDevice(), &deviceProperties);
898 const bool anisotropySupported = deviceFeatures.samplerAnisotropy == VK_TRUE;
899 const float anisotropyLevel = anisotropySupported
900 ? std::min(8.0f, deviceProperties.limits.maxSamplerAnisotropy)
901 : 1.0f;
902
903 VkSamplerCreateInfo samplerInfo{};
904 samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
905 samplerInfo.magFilter = VK_FILTER_LINEAR;
906 samplerInfo.minFilter = VK_FILTER_LINEAR;
907 samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
908 samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
909 samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
910 samplerInfo.anisotropyEnable = anisotropySupported ? VK_TRUE : VK_FALSE;
911 samplerInfo.maxAnisotropy = anisotropyLevel;
912 samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
913 samplerInfo.unnormalizedCoordinates = VK_FALSE;
914 samplerInfo.compareEnable = VK_FALSE;
915 samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
916 samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
917
918 if (vkCreateSampler(window->getDevice(), &samplerInfo, nullptr, &textureSampler) != VK_SUCCESS) {
919 throw mxvk::Exception("walk: failed to create raw pillar texture sampler");
920 }
921 }
922
923 void createDescriptorSetLayout() {
924 if (descriptorSetLayout != VK_NULL_HANDLE) {
925 return;
926 }
927
928 VkDescriptorSetLayoutBinding samplerBinding{};
929 samplerBinding.binding = 0;
930 samplerBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
931 samplerBinding.descriptorCount = 1;
932 samplerBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
933
934 VkDescriptorSetLayoutBinding uboBinding{};
935 uboBinding.binding = 1;
936 uboBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
937 uboBinding.descriptorCount = 1;
938 uboBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
939
940 const std::array<VkDescriptorSetLayoutBinding, 2> bindings = {samplerBinding, uboBinding};
941
942 VkDescriptorSetLayoutCreateInfo layoutInfo{};
943 layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
944 layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size());
945 layoutInfo.pBindings = bindings.data();
946
947 if (vkCreateDescriptorSetLayout(window->getDevice(), &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) {
948 throw mxvk::Exception("walk: failed to create raw pillar descriptor set layout");
949 }
950 }
951
952 void createUniformBuffers() {
953 destroyUniformBuffers();
954
955 const size_t frameCount = window->getSwapchainImageCount();
956 if (frameCount == 0) {
957 return;
958 }
959
960 uniformBuffers.resize(frameCount, VK_NULL_HANDLE);
961 uniformBufferMemory.resize(frameCount, VK_NULL_HANDLE);
962 uniformBuffersMapped.resize(frameCount, nullptr);
963
964 for (size_t i = 0; i < frameCount; ++i) {
965 createBuffer(sizeof(PillarUniforms),
966 VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
967 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
968 uniformBuffers[i],
969 uniformBufferMemory[i]);
970 vkMapMemory(window->getDevice(), uniformBufferMemory[i], 0, sizeof(PillarUniforms), 0, &uniformBuffersMapped[i]);
971 }
972 }
973
974 void createDescriptorPool() {
975 const uint32_t frameCount = static_cast<uint32_t>(window->getSwapchainImageCount());
976 std::array<VkDescriptorPoolSize, 2> poolSizes{};
977 poolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
978 poolSizes[0].descriptorCount = frameCount;
979 poolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
980 poolSizes[1].descriptorCount = frameCount;
981
982 VkDescriptorPoolCreateInfo poolInfo{};
983 poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
984 poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
985 poolInfo.pPoolSizes = poolSizes.data();
986 poolInfo.maxSets = frameCount;
987
988 if (vkCreateDescriptorPool(window->getDevice(), &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {
989 throw mxvk::Exception("walk: failed to create raw pillar descriptor pool");
990 }
991 }
992
993 void createDescriptorSets() {
994 const size_t frameCount = window->getSwapchainImageCount();
995 std::vector<VkDescriptorSetLayout> layouts(frameCount, descriptorSetLayout);
996
997 VkDescriptorSetAllocateInfo allocInfo{};
998 allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
999 allocInfo.descriptorPool = descriptorPool;
1000 allocInfo.descriptorSetCount = static_cast<uint32_t>(frameCount);
1001 allocInfo.pSetLayouts = layouts.data();
1002
1003 descriptorSets.resize(frameCount, VK_NULL_HANDLE);
1004 if (vkAllocateDescriptorSets(window->getDevice(), &allocInfo, descriptorSets.data()) != VK_SUCCESS) {
1005 throw mxvk::Exception("walk: failed to allocate raw pillar descriptor sets");
1006 }
1007
1008 VkDescriptorImageInfo imageInfo{};
1009 imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
1010 imageInfo.imageView = textureView;
1011 imageInfo.sampler = textureSampler;
1012
1013 for (size_t i = 0; i < frameCount; ++i) {
1014 VkDescriptorBufferInfo bufferInfo{};
1015 bufferInfo.buffer = uniformBuffers[i];
1016 bufferInfo.offset = 0;
1017 bufferInfo.range = sizeof(PillarUniforms);
1018
1019 std::array<VkWriteDescriptorSet, 2> writes{};
1020 writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1021 writes[0].dstSet = descriptorSets[i];
1022 writes[0].dstBinding = 0;
1023 writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1024 writes[0].descriptorCount = 1;
1025 writes[0].pImageInfo = &imageInfo;
1026
1027 writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1028 writes[1].dstSet = descriptorSets[i];
1029 writes[1].dstBinding = 1;
1030 writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
1031 writes[1].descriptorCount = 1;
1032 writes[1].pBufferInfo = &bufferInfo;
1033
1034 vkUpdateDescriptorSets(window->getDevice(), static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
1035 }
1036 }
1037
1038 void createPipeline() {
1039 if (descriptorSetLayout == VK_NULL_HANDLE || vertexSpv.empty() || fragmentSpv.empty() || window->getSwapchainFormat() == VK_FORMAT_UNDEFINED) {
1040 return;
1041 }
1042
1043 const VkShaderModule vertModule = mxvk::create_shader_module(window->getDevice(), vertexSpv);
1044 const VkShaderModule fragModule = mxvk::create_shader_module(window->getDevice(), fragmentSpv);
1045
1046 VkPipelineShaderStageCreateInfo vertStage{};
1047 vertStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
1048 vertStage.stage = VK_SHADER_STAGE_VERTEX_BIT;
1049 vertStage.module = vertModule;
1050 vertStage.pName = "main";
1051
1052 VkPipelineShaderStageCreateInfo fragStage{};
1053 fragStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
1054 fragStage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
1055 fragStage.module = fragModule;
1056 fragStage.pName = "main";
1057 const std::array<VkPipelineShaderStageCreateInfo, 2> stages = {vertStage, fragStage};
1058
1059 VkVertexInputBindingDescription binding{};
1060 binding.binding = 0;
1061 binding.stride = sizeof(PillarVertex);
1062 binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
1063
1064 std::array<VkVertexInputAttributeDescription, 3> attrs{};
1065 attrs[0] = {0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(PillarVertex, position)};
1066 attrs[1] = {1, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(PillarVertex, texCoord)};
1067 attrs[2] = {2, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(PillarVertex, normal)};
1068
1069 VkPipelineVertexInputStateCreateInfo vertexInput{};
1070 vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
1071 vertexInput.vertexBindingDescriptionCount = 1;
1072 vertexInput.pVertexBindingDescriptions = &binding;
1073 vertexInput.vertexAttributeDescriptionCount = static_cast<uint32_t>(attrs.size());
1074 vertexInput.pVertexAttributeDescriptions = attrs.data();
1075
1076 VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
1077 inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
1078 inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
1079
1080 const std::array<VkDynamicState, 2> dynamicStates = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
1081 VkPipelineDynamicStateCreateInfo dynamicInfo{};
1082 dynamicInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
1083 dynamicInfo.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size());
1084 dynamicInfo.pDynamicStates = dynamicStates.data();
1085
1086 VkPipelineViewportStateCreateInfo viewportState{};
1087 viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
1088 viewportState.viewportCount = 1;
1089 viewportState.scissorCount = 1;
1090
1091 VkPipelineRasterizationStateCreateInfo rasterizer{};
1092 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
1093 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
1094 // Disable face culling for the procedural pillar geometry. Winding
1095 // may differ and disabling culling prevents missing faces and flicker.
1096 rasterizer.cullMode = VK_CULL_MODE_NONE;
1097 rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
1098 rasterizer.lineWidth = 1.0f;
1099 rasterizer.depthBiasEnable = VK_FALSE;
1100
1101 VkPipelineMultisampleStateCreateInfo multisample{};
1102 multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
1103 multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
1104
1105 VkPipelineDepthStencilStateCreateInfo depthStencil{};
1106 depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
1107 depthStencil.depthTestEnable = VK_TRUE;
1108 depthStencil.depthWriteEnable = VK_TRUE;
1109 depthStencil.depthCompareOp = VK_COMPARE_OP_LESS;
1110
1111 VkPipelineColorBlendAttachmentState blendAttachment{};
1112 blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
1113 blendAttachment.blendEnable = VK_FALSE;
1114
1115 VkPipelineColorBlendStateCreateInfo colorBlend{};
1116 colorBlend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
1117 colorBlend.attachmentCount = 1;
1118 colorBlend.pAttachments = &blendAttachment;
1119
1120 VkPushConstantRange pushRange{};
1121 pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
1122 pushRange.offset = 0;
1123 pushRange.size = sizeof(glm::mat4);
1124
1125 VkPipelineLayoutCreateInfo layoutInfo{};
1126 layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
1127 layoutInfo.setLayoutCount = 1;
1128 layoutInfo.pSetLayouts = &descriptorSetLayout;
1129 layoutInfo.pushConstantRangeCount = 1;
1130 layoutInfo.pPushConstantRanges = &pushRange;
1131
1132 if (vkCreatePipelineLayout(window->getDevice(), &layoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
1133 vkDestroyShaderModule(window->getDevice(), fragModule, nullptr);
1134 vkDestroyShaderModule(window->getDevice(), vertModule, nullptr);
1135 throw mxvk::Exception("walk: failed to create raw pillar pipeline layout");
1136 }
1137
1138 const VkFormat colorFormat = window->getSwapchainFormat();
1139 const VkFormat depthFormat = window->getDepthFormat();
1140 VkPipelineRenderingCreateInfo renderingInfo{};
1141 renderingInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
1142 renderingInfo.colorAttachmentCount = 1;
1143 renderingInfo.pColorAttachmentFormats = &colorFormat;
1144 if (depthFormat != VK_FORMAT_UNDEFINED) {
1145 renderingInfo.depthAttachmentFormat = depthFormat;
1146 }
1147
1148 VkGraphicsPipelineCreateInfo pipelineInfo{};
1149 pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
1150 pipelineInfo.pNext = &renderingInfo;
1151 pipelineInfo.stageCount = static_cast<uint32_t>(stages.size());
1152 pipelineInfo.pStages = stages.data();
1153 pipelineInfo.pVertexInputState = &vertexInput;
1154 pipelineInfo.pInputAssemblyState = &inputAssembly;
1155 pipelineInfo.pViewportState = &viewportState;
1156 pipelineInfo.pRasterizationState = &rasterizer;
1157 pipelineInfo.pMultisampleState = &multisample;
1158 pipelineInfo.pDepthStencilState = &depthStencil;
1159 pipelineInfo.pColorBlendState = &colorBlend;
1160 pipelineInfo.pDynamicState = &dynamicInfo;
1161 pipelineInfo.layout = pipelineLayout;
1162 pipelineInfo.renderPass = VK_NULL_HANDLE;
1163
1164 if (vkCreateGraphicsPipelines(window->getDevice(), VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline) != VK_SUCCESS) {
1165 vkDestroyPipelineLayout(window->getDevice(), pipelineLayout, nullptr);
1166 pipelineLayout = VK_NULL_HANDLE;
1167 vkDestroyShaderModule(window->getDevice(), fragModule, nullptr);
1168 vkDestroyShaderModule(window->getDevice(), vertModule, nullptr);
1169 throw mxvk::Exception("walk: failed to create raw pillar graphics pipeline");
1170 }
1171
1172 vkDestroyShaderModule(window->getDevice(), fragModule, nullptr);
1173 vkDestroyShaderModule(window->getDevice(), vertModule, nullptr);
1174 }
1175
1176 void destroyPipeline() {
1177 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
1178 pipeline = VK_NULL_HANDLE;
1179 pipelineLayout = VK_NULL_HANDLE;
1180 return;
1181 }
1182
1183 if (pipeline != VK_NULL_HANDLE) {
1184 vkDestroyPipeline(window->getDevice(), pipeline, nullptr);
1185 pipeline = VK_NULL_HANDLE;
1186 }
1187 if (pipelineLayout != VK_NULL_HANDLE) {
1188 vkDestroyPipelineLayout(window->getDevice(), pipelineLayout, nullptr);
1189 pipelineLayout = VK_NULL_HANDLE;
1190 }
1191 }
1192
1193 void destroyDescriptors() {
1194 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
1195 descriptorSets.clear();
1196 descriptorPool = VK_NULL_HANDLE;
1197 descriptorSetLayout = VK_NULL_HANDLE;
1198 destroyUniformBuffers();
1199 return;
1200 }
1201
1202 descriptorSets.clear();
1203 if (descriptorPool != VK_NULL_HANDLE) {
1204 vkDestroyDescriptorPool(window->getDevice(), descriptorPool, nullptr);
1205 descriptorPool = VK_NULL_HANDLE;
1206 }
1207 if (descriptorSetLayout != VK_NULL_HANDLE) {
1208 vkDestroyDescriptorSetLayout(window->getDevice(), descriptorSetLayout, nullptr);
1209 descriptorSetLayout = VK_NULL_HANDLE;
1210 }
1211 destroyUniformBuffers();
1212 }
1213
1214 void destroyUniformBuffers() {
1215 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
1216 uniformBuffers.clear();
1217 uniformBufferMemory.clear();
1218 uniformBuffersMapped.clear();
1219 return;
1220 }
1221
1222 for (size_t i = 0; i < uniformBuffers.size(); ++i) {
1223 if (uniformBuffersMapped[i] != nullptr) {
1224 vkUnmapMemory(window->getDevice(), uniformBufferMemory[i]);
1225 uniformBuffersMapped[i] = nullptr;
1226 }
1227 if (uniformBuffers[i] != VK_NULL_HANDLE) {
1228 vkDestroyBuffer(window->getDevice(), uniformBuffers[i], nullptr);
1229 }
1230 if (uniformBufferMemory[i] != VK_NULL_HANDLE) {
1231 vkFreeMemory(window->getDevice(), uniformBufferMemory[i], nullptr);
1232 }
1233 }
1234
1235 uniformBuffers.clear();
1236 uniformBufferMemory.clear();
1237 uniformBuffersMapped.clear();
1238 }
1239
1240 void destroyTexture() {
1241 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
1242 textureView = VK_NULL_HANDLE;
1243 textureImage = VK_NULL_HANDLE;
1244 textureMemory = VK_NULL_HANDLE;
1245 textureSampler = VK_NULL_HANDLE;
1246 return;
1247 }
1248
1249 if (textureView != VK_NULL_HANDLE) {
1250 vkDestroyImageView(window->getDevice(), textureView, nullptr);
1251 textureView = VK_NULL_HANDLE;
1252 }
1253 if (textureImage != VK_NULL_HANDLE) {
1254 vkDestroyImage(window->getDevice(), textureImage, nullptr);
1255 textureImage = VK_NULL_HANDLE;
1256 }
1257 if (textureMemory != VK_NULL_HANDLE) {
1258 vkFreeMemory(window->getDevice(), textureMemory, nullptr);
1259 textureMemory = VK_NULL_HANDLE;
1260 }
1261 if (textureSampler != VK_NULL_HANDLE) {
1262 vkDestroySampler(window->getDevice(), textureSampler, nullptr);
1263 textureSampler = VK_NULL_HANDLE;
1264 }
1265 }
1266
1267 void destroyBuffers() {
1268 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
1269 vertexBuffer = VK_NULL_HANDLE;
1270 vertexMemory = VK_NULL_HANDLE;
1271 indexBuffer = VK_NULL_HANDLE;
1272 indexMemory = VK_NULL_HANDLE;
1273 return;
1274 }
1275
1276 if (vertexBuffer != VK_NULL_HANDLE) {
1277 vkDestroyBuffer(window->getDevice(), vertexBuffer, nullptr);
1278 vertexBuffer = VK_NULL_HANDLE;
1279 }
1280 if (vertexMemory != VK_NULL_HANDLE) {
1281 vkFreeMemory(window->getDevice(), vertexMemory, nullptr);
1282 vertexMemory = VK_NULL_HANDLE;
1283 }
1284 if (indexBuffer != VK_NULL_HANDLE) {
1285 vkDestroyBuffer(window->getDevice(), indexBuffer, nullptr);
1286 indexBuffer = VK_NULL_HANDLE;
1287 }
1288 if (indexMemory != VK_NULL_HANDLE) {
1289 vkFreeMemory(window->getDevice(), indexMemory, nullptr);
1290 indexMemory = VK_NULL_HANDLE;
1291 }
1292 }
1293
1294 void buildGeometry() {
1295 constexpr int segments = 16;
1296 constexpr float bottomCapScale = 1.5f;
1297 constexpr float baseDepth = -0.05f;
1298
1299 std::vector<float> vertices;
1300 std::vector<uint32_t> indices;
1301 vertices.reserve(128 * 8);
1302 indices.reserve(192);
1303
1304 for (int i = 0; i <= segments; ++i) {
1305 const float angle = static_cast<float>(i) / static_cast<float>(segments) * 2.0f * 3.14159265358979323846f;
1306 const float xBottom = std::cos(angle) * bottomCapScale;
1307 const float zBottom = std::sin(angle) * bottomCapScale;
1308 const float xTop = std::cos(angle);
1309 const float zTop = std::sin(angle);
1310 const float u = static_cast<float>(i) / static_cast<float>(segments);
1311 vertices.insert(vertices.end(), {
1312 xBottom,
1313 0.0f,
1314 zBottom,
1315 u,
1316 0.0f,
1317 xBottom,
1318 0.0f,
1319 zBottom,
1320 });
1321 vertices.insert(vertices.end(), {
1322 xTop,
1323 1.0f,
1324 zTop,
1325 u,
1326 1.0f,
1327 xTop,
1328 0.0f,
1329 zTop,
1330 });
1331 }
1332
1333 for (int i = 0; i < segments; ++i) {
1334 const int current = i * 2;
1335 const int next = (i + 1) * 2;
1336 indices.insert(indices.end(), {
1337 static_cast<uint32_t>(current),
1338 static_cast<uint32_t>(current + 1),
1339 static_cast<uint32_t>(next),
1340 static_cast<uint32_t>(next),
1341 static_cast<uint32_t>(current + 1),
1342 static_cast<uint32_t>(next + 1),
1343 });
1344 }
1345
1346 const uint32_t bottomCenterIndex = static_cast<uint32_t>(vertices.size() / 8);
1347 vertices.insert(vertices.end(), {
1348 0.0f,
1349 baseDepth,
1350 0.0f,
1351 0.5f,
1352 0.5f,
1353 0.0f,
1354 -1.0f,
1355 0.0f,
1356 });
1357
1358 const uint32_t bottomCapStart = static_cast<uint32_t>(vertices.size() / 8);
1359 for (int i = 0; i <= segments; ++i) {
1360 const float angle = static_cast<float>(i) / static_cast<float>(segments) * 2.0f * 3.14159265358979323846f;
1361 const float x = std::cos(angle) * bottomCapScale;
1362 const float z = std::sin(angle) * bottomCapScale;
1363 vertices.insert(vertices.end(), {
1364 x,
1365 0.0f,
1366 z,
1367 0.5f + x * 0.5f / bottomCapScale,
1368 0.5f + z * 0.5f / bottomCapScale,
1369 0.0f,
1370 -1.0f,
1371 0.0f,
1372 });
1373 }
1374 for (int i = 0; i < segments; ++i) {
1375 indices.insert(indices.end(), {
1376 bottomCenterIndex,
1377 bottomCapStart + static_cast<uint32_t>(i + 1),
1378 bottomCapStart + static_cast<uint32_t>(i),
1379 });
1380 }
1381
1382 const uint32_t topCenterIndex = static_cast<uint32_t>(vertices.size() / 8);
1383 vertices.insert(vertices.end(), {
1384 0.0f,
1385 1.0f,
1386 0.0f,
1387 0.5f,
1388 0.5f,
1389 0.0f,
1390 1.0f,
1391 0.0f,
1392 });
1393
1394 const uint32_t topCapStart = static_cast<uint32_t>(vertices.size() / 8);
1395 for (int i = 0; i <= segments; ++i) {
1396 const float angle = static_cast<float>(i) / static_cast<float>(segments) * 2.0f * 3.14159265358979323846f;
1397 const float x = std::cos(angle);
1398 const float z = std::sin(angle);
1399 vertices.insert(vertices.end(), {
1400 x,
1401 1.0f,
1402 z,
1403 0.5f + x * 0.5f,
1404 0.5f + z * 0.5f,
1405 0.0f,
1406 1.0f,
1407 0.0f,
1408 });
1409 }
1410 for (int i = 0; i < segments; ++i) {
1411 indices.insert(indices.end(), {
1412 topCenterIndex,
1413 topCapStart + static_cast<uint32_t>(i),
1414 topCapStart + static_cast<uint32_t>(i + 1),
1415 });
1416 }
1417
1418 vertexCount = static_cast<uint32_t>(vertices.size() / 8);
1419 indexCount = static_cast<uint32_t>(indices.size());
1420
1421 std::vector<PillarVertex> pillarVertices(vertexCount);
1422 for (uint32_t i = 0; i < vertexCount; ++i) {
1423 const size_t base = static_cast<size_t>(i) * 8;
1424 pillarVertices[i].position = glm::vec3(vertices[base + 0], vertices[base + 1], vertices[base + 2]);
1425 pillarVertices[i].texCoord = glm::vec2(vertices[base + 3], vertices[base + 4]);
1426 pillarVertices[i].normal = glm::vec3(vertices[base + 5], vertices[base + 6], vertices[base + 7]);
1427 }
1428
1429 createBuffer(static_cast<VkDeviceSize>(pillarVertices.size() * sizeof(PillarVertex)),
1430 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
1431 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1432 vertexBuffer,
1433 vertexMemory);
1434 void *mapped = nullptr;
1435 vkMapMemory(window->getDevice(), vertexMemory, 0, VK_WHOLE_SIZE, 0, &mapped);
1436 std::memcpy(mapped, pillarVertices.data(), pillarVertices.size() * sizeof(PillarVertex));
1437 vkUnmapMemory(window->getDevice(), vertexMemory);
1438
1439 createBuffer(static_cast<VkDeviceSize>(indices.size() * sizeof(uint32_t)),
1440 VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
1441 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1442 indexBuffer,
1443 indexMemory);
1444 vkMapMemory(window->getDevice(), indexMemory, 0, VK_WHOLE_SIZE, 0, &mapped);
1445 std::memcpy(mapped, indices.data(), indices.size() * sizeof(uint32_t));
1446 vkUnmapMemory(window->getDevice(), indexMemory);
1447 }
1448
1449 void loadTexture([[maybe_unused]] const std::string &textureManifestPath, const std::string &textureBasePath) {
1450 SDL_Surface *surface = mxvk::LoadPNG((textureBasePath + "/ground.png").c_str());
1451 if (surface == nullptr) {
1452 throw mxvk::Exception("walk: failed to load raw pillar texture");
1453 }
1454
1455 const uint32_t width = static_cast<uint32_t>(surface->w);
1456 const uint32_t height = static_cast<uint32_t>(surface->h);
1457 const VkDeviceSize imageSize = static_cast<VkDeviceSize>(width) * static_cast<VkDeviceSize>(height) * 4U;
1458
1459 VkBuffer stagingBuffer = VK_NULL_HANDLE;
1460 VkDeviceMemory stagingMemory = VK_NULL_HANDLE;
1461 createBuffer(imageSize,
1462 VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
1463 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1464 stagingBuffer,
1465 stagingMemory);
1466
1467 void *mapped = nullptr;
1468 vkMapMemory(window->getDevice(), stagingMemory, 0, imageSize, 0, &mapped);
1469 std::memcpy(mapped, surface->pixels, static_cast<size_t>(imageSize));
1470 vkUnmapMemory(window->getDevice(), stagingMemory);
1471
1472 createImage(width, height,
1473 VK_FORMAT_R8G8B8A8_UNORM,
1474 VK_IMAGE_TILING_OPTIMAL,
1475 VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
1476 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
1477 textureImage,
1478 textureMemory);
1479
1480 transitionImageLayout(textureImage, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
1481 copyBufferToImage(stagingBuffer, textureImage, width, height);
1482 transitionImageLayout(textureImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
1483
1484 textureView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT);
1485
1486 vkDestroyBuffer(window->getDevice(), stagingBuffer, nullptr);
1487 vkFreeMemory(window->getDevice(), stagingMemory, nullptr);
1488 SDL_DestroySurface(surface);
1489 }
1490
1491 mxvk::VK_Window *window = nullptr;
1492 std::vector<char> vertexSpv{};
1493 std::vector<char> fragmentSpv{};
1494
1495 uint32_t vertexCount = 0;
1496 uint32_t indexCount = 0;
1497 VkBuffer vertexBuffer = VK_NULL_HANDLE;
1498 VkDeviceMemory vertexMemory = VK_NULL_HANDLE;
1499 VkBuffer indexBuffer = VK_NULL_HANDLE;
1500 VkDeviceMemory indexMemory = VK_NULL_HANDLE;
1501
1502 VkImage textureImage = VK_NULL_HANDLE;
1503 VkDeviceMemory textureMemory = VK_NULL_HANDLE;
1504 VkImageView textureView = VK_NULL_HANDLE;
1505 VkSampler textureSampler = VK_NULL_HANDLE;
1506
1507 VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
1508 VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
1509 std::vector<VkDescriptorSet> descriptorSets{};
1510
1511 VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
1512 VkPipeline pipeline = VK_NULL_HANDLE;
1513
1514 std::vector<VkBuffer> uniformBuffers{};
1515 std::vector<VkDeviceMemory> uniformBufferMemory{};
1516 std::vector<void *> uniformBuffersMapped{};
1517 };
1518
1520 public:
1521 struct WallVertex {
1522 glm::vec3 position{0.0f};
1523 glm::vec2 texCoord{0.0f};
1524 glm::vec3 normal{0.0f};
1525 };
1526
1528 glm::mat4 view{1.0f};
1529 glm::mat4 proj{1.0f};
1530 glm::vec4 fx{0.0f};
1531 };
1532
1533 void load(mxvk::VK_Window *targetWindow,
1534 const std::string &textureManifestPath,
1535 const std::string &textureBasePath,
1536 const std::vector<char> &vertexShaderSpv,
1537 const std::vector<char> &fragmentShaderSpv) {
1538 if (targetWindow == nullptr) {
1539 throw mxvk::Exception("walk: raw wall renderer requires a valid window");
1540 }
1541 window = targetWindow;
1542 vertSpv = vertexShaderSpv;
1543 fragSpv = fragmentShaderSpv;
1544
1545 if (!window->ensureRenderResources()) {
1546 throw mxvk::Exception("walk: raw wall renderer requires render resources");
1547 }
1548
1549 buildGeometry();
1550 loadTexture(textureManifestPath, textureBasePath);
1551 createTextureSampler();
1552 createDescriptorSetLayout();
1553 createUniformBuffers();
1554 createDescriptorPool();
1555 createDescriptorSets();
1556 createPipeline();
1557 }
1558
1559 void resize(mxvk::VK_Window *targetWindow) {
1560 if (targetWindow == nullptr || targetWindow->getDevice() == VK_NULL_HANDLE) {
1561 return;
1562 }
1563
1564 window = targetWindow;
1565 destroyPipeline();
1566 destroyDescriptors();
1567 createDescriptorSetLayout();
1568 createUniformBuffers();
1569 createDescriptorPool();
1570 createDescriptorSets();
1571 createPipeline();
1572 }
1573
1574 /// @brief Hot-swap the fragment shader without rebuilding geometry or descriptors.
1575 /// @param newFragSpv Compiled SPIR-V bytecode for the new fragment shader.
1576 void reloadFragShader(const std::vector<char> &newFragSpv) {
1577 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE || newFragSpv.empty()) {
1578 return;
1579 }
1580 vkDeviceWaitIdle(window->getDevice());
1581 fragSpv = newFragSpv;
1582 destroyPipeline();
1583 createPipeline();
1584 }
1585
1586 void cleanup(mxvk::VK_Window *targetWindow) {
1587 if (targetWindow == nullptr || targetWindow->getDevice() == VK_NULL_HANDLE) {
1588 return;
1589 }
1590
1591 window = targetWindow;
1592 destroyPipeline();
1593 destroyDescriptors();
1594 destroyTexture();
1595 destroyBuffers();
1596 window = nullptr;
1597 }
1598
1599 void render(VkCommandBuffer cmd,
1600 uint32_t imageIndex,
1601 const std::vector<WallSegment> &walls,
1602 float wallThickness,
1603 const glm::mat4 &view,
1604 const glm::mat4 &proj,
1605 const glm::vec4 &fx) {
1606 if (cmd == VK_NULL_HANDLE || pipeline == VK_NULL_HANDLE || pipelineLayout == VK_NULL_HANDLE) {
1607 return;
1608 }
1609 if (imageIndex >= uniformBuffersMapped.size() || descriptorSets.empty() || vertexBuffer == VK_NULL_HANDLE || indexBuffer == VK_NULL_HANDLE) {
1610 return;
1611 }
1612
1613 WallUniforms uniforms{};
1614 uniforms.view = view;
1615 uniforms.proj = proj;
1616 uniforms.fx = fx;
1617 std::memcpy(uniformBuffersMapped[imageIndex], &uniforms, sizeof(WallUniforms));
1618
1619 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
1620 vkCmdBindDescriptorSets(cmd,
1621 VK_PIPELINE_BIND_POINT_GRAPHICS,
1622 pipelineLayout,
1623 0,
1624 1,
1625 &descriptorSets[imageIndex],
1626 0,
1627 nullptr);
1628
1629 const VkDeviceSize offset = 0;
1630 vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset);
1631 vkCmdBindIndexBuffer(cmd, indexBuffer, 0, VK_INDEX_TYPE_UINT32);
1632
1633 const float thickness = std::max(0.02f, wallThickness);
1634 // Extend each wall by about half its thickness on both ends so adjoining
1635 // runs meet cleanly without visible seam slivers.
1636 const float wallOverlap = thickness * 0.55f;
1637 for (const WallSegment &segment : walls) {
1638 const glm::vec3 center = (segment.start + segment.end) * 0.5f;
1639 const glm::vec3 span = segment.end - segment.start;
1640 const float length = glm::length(span);
1641 if (length < 0.0001f) {
1642 continue;
1643 }
1644 // Unit wall geometry has Y in [0..1]. Sink slightly to avoid floor/wall
1645 // depth fighting where the floor top plane is also at y=0.
1646 constexpr float baseSink = 0.01f;
1647 glm::mat4 model = glm::translate(glm::mat4(1.0f), glm::vec3(center.x, -baseSink, center.z));
1648 model = glm::rotate(model, std::atan2(span.z, span.x), glm::vec3(0.0f, 1.0f, 0.0f));
1649 model = glm::scale(model, glm::vec3(length + wallOverlap * 2.0f, segment.height, thickness));
1650 vkCmdPushConstants(cmd, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(glm::mat4), &model);
1651 vkCmdDrawIndexed(cmd, indexCount, 1, 0, 0, 0);
1652 }
1653 }
1654
1655 private:
1656 [[nodiscard]] uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) const {
1657 VkPhysicalDeviceMemoryProperties memProperties{};
1658 vkGetPhysicalDeviceMemoryProperties(window->getPhysicalDevice(), &memProperties);
1659 for (uint32_t i = 0; i < memProperties.memoryTypeCount; ++i) {
1660 if ((typeFilter & (1u << i)) != 0u && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
1661 return i;
1662 }
1663 }
1664 throw mxvk::Exception("walk: failed to find suitable memory type for raw wall renderer");
1665 }
1666
1667 void createBuffer(VkDeviceSize size,
1668 VkBufferUsageFlags usage,
1669 VkMemoryPropertyFlags properties,
1670 VkBuffer &buffer,
1671 VkDeviceMemory &bufferMemory) const {
1672 VkBufferCreateInfo bufferInfo{};
1673 bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
1674 bufferInfo.size = size;
1675 bufferInfo.usage = usage;
1676 bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
1677
1678 if (vkCreateBuffer(window->getDevice(), &bufferInfo, nullptr, &buffer) != VK_SUCCESS) {
1679 throw mxvk::Exception("walk: failed to create raw wall buffer");
1680 }
1681
1682 VkMemoryRequirements requirements{};
1683 vkGetBufferMemoryRequirements(window->getDevice(), buffer, &requirements);
1684
1685 VkMemoryAllocateInfo allocInfo{};
1686 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
1687 allocInfo.allocationSize = requirements.size;
1688
1689 try {
1690 allocInfo.memoryTypeIndex = findMemoryType(requirements.memoryTypeBits, properties);
1691 if (vkAllocateMemory(window->getDevice(), &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) {
1692 throw mxvk::Exception("walk: failed to allocate raw wall buffer memory");
1693 }
1694
1695 if (vkBindBufferMemory(window->getDevice(), buffer, bufferMemory, 0) != VK_SUCCESS) {
1696 throw mxvk::Exception("walk: failed to bind raw wall buffer memory");
1697 }
1698 } catch (...) {
1699 if (bufferMemory != VK_NULL_HANDLE) {
1700 vkFreeMemory(window->getDevice(), bufferMemory, nullptr);
1701 bufferMemory = VK_NULL_HANDLE;
1702 }
1703 if (buffer != VK_NULL_HANDLE) {
1704 vkDestroyBuffer(window->getDevice(), buffer, nullptr);
1705 buffer = VK_NULL_HANDLE;
1706 }
1707 throw;
1708 }
1709 }
1710
1711 void buildGeometry() {
1712 // Unit wall prism: X in [-0.5..0.5], Y in [0..1], Z in [-0.5..0.5].
1713 // Per-instance scaling in render() controls segment length/height/thickness.
1714 std::vector<WallVertex> verts;
1715 std::vector<uint32_t> inds;
1716 verts.reserve(24);
1717 inds.reserve(36);
1718
1719 const auto addFace = [&verts, &inds](const glm::vec3 &v0,
1720 const glm::vec3 &v1,
1721 const glm::vec3 &v2,
1722 const glm::vec3 &v3,
1723 const glm::vec3 &normal) {
1724 const uint32_t base = static_cast<uint32_t>(verts.size());
1725 verts.push_back({v0, glm::vec2(0.0f, 0.0f), normal});
1726 verts.push_back({v1, glm::vec2(1.0f, 0.0f), normal});
1727 verts.push_back({v2, glm::vec2(1.0f, 1.0f), normal});
1728 verts.push_back({v3, glm::vec2(0.0f, 1.0f), normal});
1729 inds.insert(inds.end(), {base + 0, base + 1, base + 2, base + 2, base + 3, base + 0});
1730 };
1731
1732 constexpr float x = 0.5f;
1733 constexpr float z = 0.5f;
1734 constexpr float y0 = 0.0f;
1735 constexpr float y1 = 1.0f;
1736
1737 addFace(glm::vec3(-x, y0, z), glm::vec3(x, y0, z), glm::vec3(x, y1, z), glm::vec3(-x, y1, z), glm::vec3(0.0f, 0.0f, 1.0f));
1738 addFace(glm::vec3(x, y0, -z), glm::vec3(-x, y0, -z), glm::vec3(-x, y1, -z), glm::vec3(x, y1, -z), glm::vec3(0.0f, 0.0f, -1.0f));
1739 addFace(glm::vec3(x, y0, z), glm::vec3(x, y0, -z), glm::vec3(x, y1, -z), glm::vec3(x, y1, z), glm::vec3(1.0f, 0.0f, 0.0f));
1740 addFace(glm::vec3(-x, y0, -z), glm::vec3(-x, y0, z), glm::vec3(-x, y1, z), glm::vec3(-x, y1, -z), glm::vec3(-1.0f, 0.0f, 0.0f));
1741 addFace(glm::vec3(-x, y1, z), glm::vec3(x, y1, z), glm::vec3(x, y1, -z), glm::vec3(-x, y1, -z), glm::vec3(0.0f, 1.0f, 0.0f));
1742 addFace(glm::vec3(-x, y0, -z), glm::vec3(x, y0, -z), glm::vec3(x, y0, z), glm::vec3(-x, y0, z), glm::vec3(0.0f, -1.0f, 0.0f));
1743
1744 vertexCount = static_cast<uint32_t>(verts.size());
1745 indexCount = static_cast<uint32_t>(inds.size());
1746
1747 createBuffer(static_cast<VkDeviceSize>(verts.size() * sizeof(WallVertex)),
1748 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
1749 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1750 vertexBuffer,
1751 vertexMemory);
1752 void *mapped = nullptr;
1753 vkMapMemory(window->getDevice(), vertexMemory, 0, VK_WHOLE_SIZE, 0, &mapped);
1754 std::memcpy(mapped, verts.data(), verts.size() * sizeof(WallVertex));
1755 vkUnmapMemory(window->getDevice(), vertexMemory);
1756
1757 createBuffer(static_cast<VkDeviceSize>(inds.size() * sizeof(uint32_t)),
1758 VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
1759 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1760 indexBuffer,
1761 indexMemory);
1762 vkMapMemory(window->getDevice(), indexMemory, 0, VK_WHOLE_SIZE, 0, &mapped);
1763 std::memcpy(mapped, inds.data(), inds.size() * sizeof(uint32_t));
1764 vkUnmapMemory(window->getDevice(), indexMemory);
1765 }
1766
1767 void loadTexture([[maybe_unused]] const std::string &textureManifestPath, const std::string &textureBasePath) {
1768 SDL_Surface *surface = mxvk::LoadPNG((textureBasePath + "/wall_bricks.png").c_str());
1769 if (surface == nullptr) {
1770 throw mxvk::Exception("walk: failed to load raw wall texture");
1771 }
1772
1773 const uint32_t width = static_cast<uint32_t>(surface->w);
1774 const uint32_t height = static_cast<uint32_t>(surface->h);
1775 const VkDeviceSize imageSize = static_cast<VkDeviceSize>(width) * static_cast<VkDeviceSize>(height) * 4U;
1776
1777 VkBuffer stagingBuffer = VK_NULL_HANDLE;
1778 VkDeviceMemory stagingMemory = VK_NULL_HANDLE;
1779 createBuffer(imageSize,
1780 VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
1781 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1782 stagingBuffer,
1783 stagingMemory);
1784
1785 void *mapped = nullptr;
1786 vkMapMemory(window->getDevice(), stagingMemory, 0, imageSize, 0, &mapped);
1787 std::memcpy(mapped, surface->pixels, static_cast<size_t>(imageSize));
1788 vkUnmapMemory(window->getDevice(), stagingMemory);
1789
1790 createImage(width, height,
1791 VK_FORMAT_R8G8B8A8_UNORM,
1792 VK_IMAGE_TILING_OPTIMAL,
1793 VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
1794 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
1795 textureImage,
1796 textureMemory);
1797
1798 transitionImageLayout(textureImage, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
1799 copyBufferToImage(stagingBuffer, textureImage, width, height);
1800 transitionImageLayout(textureImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
1801
1802 textureView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT);
1803
1804 vkDestroyBuffer(window->getDevice(), stagingBuffer, nullptr);
1805 vkFreeMemory(window->getDevice(), stagingMemory, nullptr);
1806 SDL_DestroySurface(surface);
1807 }
1808
1809 void createTextureSampler() {
1810 if (textureSampler != VK_NULL_HANDLE) {
1811 return;
1812 }
1813
1814 VkPhysicalDeviceFeatures deviceFeatures{};
1815 vkGetPhysicalDeviceFeatures(window->getPhysicalDevice(), &deviceFeatures);
1816 VkPhysicalDeviceProperties deviceProperties{};
1817 vkGetPhysicalDeviceProperties(window->getPhysicalDevice(), &deviceProperties);
1818 const bool anisotropySupported = deviceFeatures.samplerAnisotropy == VK_TRUE;
1819 const float anisotropyLevel = anisotropySupported
1820 ? std::min(8.0f, deviceProperties.limits.maxSamplerAnisotropy)
1821 : 1.0f;
1822
1823 VkSamplerCreateInfo samplerInfo{};
1824 samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
1825 samplerInfo.magFilter = VK_FILTER_LINEAR;
1826 samplerInfo.minFilter = VK_FILTER_LINEAR;
1827 samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
1828 samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
1829 samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
1830 samplerInfo.anisotropyEnable = anisotropySupported ? VK_TRUE : VK_FALSE;
1831 samplerInfo.maxAnisotropy = anisotropyLevel;
1832 samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
1833 samplerInfo.unnormalizedCoordinates = VK_FALSE;
1834 samplerInfo.compareEnable = VK_FALSE;
1835 samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
1836 samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
1837
1838 if (vkCreateSampler(window->getDevice(), &samplerInfo, nullptr, &textureSampler) != VK_SUCCESS) {
1839 throw mxvk::Exception("walk: failed to create raw wall texture sampler");
1840 }
1841 }
1842
1843 void createDescriptorSetLayout() {
1844 if (descriptorSetLayout != VK_NULL_HANDLE) {
1845 return;
1846 }
1847
1848 VkDescriptorSetLayoutBinding samplerBinding{};
1849 samplerBinding.binding = 0;
1850 samplerBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1851 samplerBinding.descriptorCount = 1;
1852 samplerBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
1853
1854 VkDescriptorSetLayoutBinding uboBinding{};
1855 uboBinding.binding = 1;
1856 uboBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
1857 uboBinding.descriptorCount = 1;
1858 uboBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
1859
1860 const std::array<VkDescriptorSetLayoutBinding, 2> bindings = {samplerBinding, uboBinding};
1861
1862 VkDescriptorSetLayoutCreateInfo layoutInfo{};
1863 layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
1864 layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size());
1865 layoutInfo.pBindings = bindings.data();
1866
1867 if (vkCreateDescriptorSetLayout(window->getDevice(), &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) {
1868 throw mxvk::Exception("walk: failed to create raw wall descriptor set layout");
1869 }
1870 }
1871
1872 void createUniformBuffers() {
1873 destroyUniformBuffers();
1874
1875 const size_t frameCount = window->getSwapchainImageCount();
1876 if (frameCount == 0) {
1877 return;
1878 }
1879
1880 uniformBuffers.resize(frameCount, VK_NULL_HANDLE);
1881 uniformBufferMemory.resize(frameCount, VK_NULL_HANDLE);
1882 uniformBuffersMapped.resize(frameCount, nullptr);
1883
1884 for (size_t i = 0; i < frameCount; ++i) {
1885 createBuffer(sizeof(WallUniforms),
1886 VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
1887 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
1888 uniformBuffers[i],
1889 uniformBufferMemory[i]);
1890 vkMapMemory(window->getDevice(), uniformBufferMemory[i], 0, sizeof(WallUniforms), 0, &uniformBuffersMapped[i]);
1891 }
1892 }
1893
1894 void createDescriptorPool() {
1895 const uint32_t frameCount = static_cast<uint32_t>(window->getSwapchainImageCount());
1896 std::array<VkDescriptorPoolSize, 2> poolSizes{};
1897 poolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1898 poolSizes[0].descriptorCount = frameCount;
1899 poolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
1900 poolSizes[1].descriptorCount = frameCount;
1901
1902 VkDescriptorPoolCreateInfo poolInfo{};
1903 poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
1904 poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
1905 poolInfo.pPoolSizes = poolSizes.data();
1906 poolInfo.maxSets = frameCount;
1907
1908 if (vkCreateDescriptorPool(window->getDevice(), &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {
1909 throw mxvk::Exception("walk: failed to create raw wall descriptor pool");
1910 }
1911 }
1912
1913 void createDescriptorSets() {
1914 const size_t frameCount = window->getSwapchainImageCount();
1915 std::vector<VkDescriptorSetLayout> layouts(frameCount, descriptorSetLayout);
1916
1917 VkDescriptorSetAllocateInfo allocInfo{};
1918 allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
1919 allocInfo.descriptorPool = descriptorPool;
1920 allocInfo.descriptorSetCount = static_cast<uint32_t>(frameCount);
1921 allocInfo.pSetLayouts = layouts.data();
1922
1923 descriptorSets.resize(frameCount, VK_NULL_HANDLE);
1924 if (vkAllocateDescriptorSets(window->getDevice(), &allocInfo, descriptorSets.data()) != VK_SUCCESS) {
1925 throw mxvk::Exception("walk: failed to allocate raw wall descriptor sets");
1926 }
1927
1928 VkDescriptorImageInfo imageInfo{};
1929 imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
1930 imageInfo.imageView = textureView;
1931 imageInfo.sampler = textureSampler;
1932
1933 for (size_t i = 0; i < frameCount; ++i) {
1934 VkDescriptorBufferInfo bufferInfo{};
1935 bufferInfo.buffer = uniformBuffers[i];
1936 bufferInfo.offset = 0;
1937 bufferInfo.range = sizeof(WallUniforms);
1938
1939 std::array<VkWriteDescriptorSet, 2> writes{};
1940 writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1941 writes[0].dstSet = descriptorSets[i];
1942 writes[0].dstBinding = 0;
1943 writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
1944 writes[0].descriptorCount = 1;
1945 writes[0].pImageInfo = &imageInfo;
1946
1947 writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
1948 writes[1].dstSet = descriptorSets[i];
1949 writes[1].dstBinding = 1;
1950 writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
1951 writes[1].descriptorCount = 1;
1952 writes[1].pBufferInfo = &bufferInfo;
1953
1954 vkUpdateDescriptorSets(window->getDevice(), static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
1955 }
1956 }
1957
1958 void createPipeline() {
1959 if (descriptorSetLayout == VK_NULL_HANDLE || vertSpv.empty() || fragSpv.empty() || window->getSwapchainFormat() == VK_FORMAT_UNDEFINED) {
1960 return;
1961 }
1962
1963 const VkShaderModule vertModule = mxvk::create_shader_module(window->getDevice(), vertSpv);
1964 const VkShaderModule fragModule = mxvk::create_shader_module(window->getDevice(), fragSpv);
1965
1966 VkPipelineShaderStageCreateInfo vertStage{};
1967 vertStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
1968 vertStage.stage = VK_SHADER_STAGE_VERTEX_BIT;
1969 vertStage.module = vertModule;
1970 vertStage.pName = "main";
1971
1972 VkPipelineShaderStageCreateInfo fragStage{};
1973 fragStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
1974 fragStage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
1975 fragStage.module = fragModule;
1976 fragStage.pName = "main";
1977 const std::array<VkPipelineShaderStageCreateInfo, 2> stages = {vertStage, fragStage};
1978
1979 VkVertexInputBindingDescription binding{};
1980 binding.binding = 0;
1981 binding.stride = sizeof(WallVertex);
1982 binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
1983
1984 std::array<VkVertexInputAttributeDescription, 3> attrs{};
1985 attrs[0] = {0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(WallVertex, position)};
1986 attrs[1] = {1, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(WallVertex, texCoord)};
1987 attrs[2] = {2, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(WallVertex, normal)};
1988
1989 VkPipelineVertexInputStateCreateInfo vertexInput{};
1990 vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
1991 vertexInput.vertexBindingDescriptionCount = 1;
1992 vertexInput.pVertexBindingDescriptions = &binding;
1993 vertexInput.vertexAttributeDescriptionCount = static_cast<uint32_t>(attrs.size());
1994 vertexInput.pVertexAttributeDescriptions = attrs.data();
1995
1996 VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
1997 inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
1998 inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
1999
2000 const std::array<VkDynamicState, 2> dynamicStates = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
2001 VkPipelineDynamicStateCreateInfo dynamicInfo{};
2002 dynamicInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
2003 dynamicInfo.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size());
2004 dynamicInfo.pDynamicStates = dynamicStates.data();
2005
2006 VkPipelineViewportStateCreateInfo viewportState{};
2007 viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
2008 viewportState.viewportCount = 1;
2009 viewportState.scissorCount = 1;
2010
2011 VkPipelineRasterizationStateCreateInfo rasterizer{};
2012 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
2013 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
2014 rasterizer.cullMode = VK_CULL_MODE_NONE;
2015 rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
2016 rasterizer.lineWidth = 1.0f;
2017
2018 VkPipelineMultisampleStateCreateInfo multisample{};
2019 multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
2020 multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
2021
2022 VkPipelineDepthStencilStateCreateInfo depthStencil{};
2023 depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
2024 depthStencil.depthTestEnable = VK_TRUE;
2025 depthStencil.depthWriteEnable = VK_TRUE;
2026 depthStencil.depthCompareOp = VK_COMPARE_OP_LESS;
2027
2028 VkPipelineColorBlendAttachmentState blendAttachment{};
2029 blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
2030 blendAttachment.blendEnable = VK_FALSE;
2031
2032 VkPipelineColorBlendStateCreateInfo colorBlend{};
2033 colorBlend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
2034 colorBlend.attachmentCount = 1;
2035 colorBlend.pAttachments = &blendAttachment;
2036
2037 VkPushConstantRange pushRange{};
2038 pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
2039 pushRange.offset = 0;
2040 pushRange.size = sizeof(glm::mat4);
2041
2042 VkPipelineLayoutCreateInfo layoutInfo{};
2043 layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
2044 layoutInfo.setLayoutCount = 1;
2045 layoutInfo.pSetLayouts = &descriptorSetLayout;
2046 layoutInfo.pushConstantRangeCount = 1;
2047 layoutInfo.pPushConstantRanges = &pushRange;
2048
2049 if (vkCreatePipelineLayout(window->getDevice(), &layoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
2050 vkDestroyShaderModule(window->getDevice(), fragModule, nullptr);
2051 vkDestroyShaderModule(window->getDevice(), vertModule, nullptr);
2052 throw mxvk::Exception("walk: failed to create raw wall pipeline layout");
2053 }
2054
2055 const VkFormat colorFormat = window->getSwapchainFormat();
2056 const VkFormat depthFormat = window->getDepthFormat();
2057 VkPipelineRenderingCreateInfo renderingInfo{};
2058 renderingInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
2059 renderingInfo.colorAttachmentCount = 1;
2060 renderingInfo.pColorAttachmentFormats = &colorFormat;
2061 if (depthFormat != VK_FORMAT_UNDEFINED) {
2062 renderingInfo.depthAttachmentFormat = depthFormat;
2063 }
2064
2065 VkGraphicsPipelineCreateInfo pipelineInfo{};
2066 pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
2067 pipelineInfo.pNext = &renderingInfo;
2068 pipelineInfo.stageCount = static_cast<uint32_t>(stages.size());
2069 pipelineInfo.pStages = stages.data();
2070 pipelineInfo.pVertexInputState = &vertexInput;
2071 pipelineInfo.pInputAssemblyState = &inputAssembly;
2072 pipelineInfo.pViewportState = &viewportState;
2073 pipelineInfo.pRasterizationState = &rasterizer;
2074 pipelineInfo.pMultisampleState = &multisample;
2075 pipelineInfo.pDepthStencilState = &depthStencil;
2076 pipelineInfo.pColorBlendState = &colorBlend;
2077 pipelineInfo.pDynamicState = &dynamicInfo;
2078 pipelineInfo.layout = pipelineLayout;
2079 pipelineInfo.renderPass = VK_NULL_HANDLE;
2080
2081 if (vkCreateGraphicsPipelines(window->getDevice(), VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline) != VK_SUCCESS) {
2082 vkDestroyPipelineLayout(window->getDevice(), pipelineLayout, nullptr);
2083 pipelineLayout = VK_NULL_HANDLE;
2084 vkDestroyShaderModule(window->getDevice(), fragModule, nullptr);
2085 vkDestroyShaderModule(window->getDevice(), vertModule, nullptr);
2086 throw mxvk::Exception("walk: failed to create raw wall graphics pipeline");
2087 }
2088
2089 vkDestroyShaderModule(window->getDevice(), fragModule, nullptr);
2090 vkDestroyShaderModule(window->getDevice(), vertModule, nullptr);
2091 }
2092
2093 void destroyPipeline() {
2094 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
2095 pipeline = VK_NULL_HANDLE;
2096 pipelineLayout = VK_NULL_HANDLE;
2097 return;
2098 }
2099
2100 if (pipeline != VK_NULL_HANDLE) {
2101 vkDestroyPipeline(window->getDevice(), pipeline, nullptr);
2102 pipeline = VK_NULL_HANDLE;
2103 }
2104 if (pipelineLayout != VK_NULL_HANDLE) {
2105 vkDestroyPipelineLayout(window->getDevice(), pipelineLayout, nullptr);
2106 pipelineLayout = VK_NULL_HANDLE;
2107 }
2108 }
2109
2110 void destroyDescriptors() {
2111 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
2112 descriptorSets.clear();
2113 descriptorPool = VK_NULL_HANDLE;
2114 descriptorSetLayout = VK_NULL_HANDLE;
2115 destroyUniformBuffers();
2116 return;
2117 }
2118
2119 descriptorSets.clear();
2120 if (descriptorPool != VK_NULL_HANDLE) {
2121 vkDestroyDescriptorPool(window->getDevice(), descriptorPool, nullptr);
2122 descriptorPool = VK_NULL_HANDLE;
2123 }
2124 if (descriptorSetLayout != VK_NULL_HANDLE) {
2125 vkDestroyDescriptorSetLayout(window->getDevice(), descriptorSetLayout, nullptr);
2126 descriptorSetLayout = VK_NULL_HANDLE;
2127 }
2128 destroyUniformBuffers();
2129 }
2130
2131 void destroyUniformBuffers() {
2132 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
2133 uniformBuffers.clear();
2134 uniformBufferMemory.clear();
2135 uniformBuffersMapped.clear();
2136 return;
2137 }
2138
2139 for (size_t i = 0; i < uniformBuffers.size(); ++i) {
2140 if (uniformBuffersMapped[i] != nullptr) {
2141 vkUnmapMemory(window->getDevice(), uniformBufferMemory[i]);
2142 uniformBuffersMapped[i] = nullptr;
2143 }
2144 if (uniformBuffers[i] != VK_NULL_HANDLE) {
2145 vkDestroyBuffer(window->getDevice(), uniformBuffers[i], nullptr);
2146 }
2147 if (uniformBufferMemory[i] != VK_NULL_HANDLE) {
2148 vkFreeMemory(window->getDevice(), uniformBufferMemory[i], nullptr);
2149 }
2150 }
2151
2152 uniformBuffers.clear();
2153 uniformBufferMemory.clear();
2154 uniformBuffersMapped.clear();
2155 }
2156
2157 void destroyTexture() {
2158 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
2159 textureView = VK_NULL_HANDLE;
2160 textureImage = VK_NULL_HANDLE;
2161 textureMemory = VK_NULL_HANDLE;
2162 textureSampler = VK_NULL_HANDLE;
2163 return;
2164 }
2165
2166 if (textureView != VK_NULL_HANDLE) {
2167 vkDestroyImageView(window->getDevice(), textureView, nullptr);
2168 textureView = VK_NULL_HANDLE;
2169 }
2170 if (textureImage != VK_NULL_HANDLE) {
2171 vkDestroyImage(window->getDevice(), textureImage, nullptr);
2172 textureImage = VK_NULL_HANDLE;
2173 }
2174 if (textureMemory != VK_NULL_HANDLE) {
2175 vkFreeMemory(window->getDevice(), textureMemory, nullptr);
2176 textureMemory = VK_NULL_HANDLE;
2177 }
2178 if (textureSampler != VK_NULL_HANDLE) {
2179 vkDestroySampler(window->getDevice(), textureSampler, nullptr);
2180 textureSampler = VK_NULL_HANDLE;
2181 }
2182 }
2183
2184 void destroyBuffers() {
2185 if (window == nullptr || window->getDevice() == VK_NULL_HANDLE) {
2186 vertexBuffer = VK_NULL_HANDLE;
2187 vertexMemory = VK_NULL_HANDLE;
2188 indexBuffer = VK_NULL_HANDLE;
2189 indexMemory = VK_NULL_HANDLE;
2190 return;
2191 }
2192
2193 if (vertexBuffer != VK_NULL_HANDLE) {
2194 vkDestroyBuffer(window->getDevice(), vertexBuffer, nullptr);
2195 vertexBuffer = VK_NULL_HANDLE;
2196 }
2197 if (vertexMemory != VK_NULL_HANDLE) {
2198 vkFreeMemory(window->getDevice(), vertexMemory, nullptr);
2199 vertexMemory = VK_NULL_HANDLE;
2200 }
2201 if (indexBuffer != VK_NULL_HANDLE) {
2202 vkDestroyBuffer(window->getDevice(), indexBuffer, nullptr);
2203 indexBuffer = VK_NULL_HANDLE;
2204 }
2205 if (indexMemory != VK_NULL_HANDLE) {
2206 vkFreeMemory(window->getDevice(), indexMemory, nullptr);
2207 indexMemory = VK_NULL_HANDLE;
2208 }
2209 }
2210
2211 void createImage(uint32_t width,
2212 uint32_t height,
2213 VkFormat format,
2214 VkImageTiling tiling,
2215 VkImageUsageFlags usage,
2216 VkMemoryPropertyFlags properties,
2217 VkImage &image,
2218 VkDeviceMemory &memory) const {
2219 VkImageCreateInfo imageInfo{};
2220 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
2221 imageInfo.imageType = VK_IMAGE_TYPE_2D;
2222 imageInfo.extent.width = width;
2223 imageInfo.extent.height = height;
2224 imageInfo.extent.depth = 1;
2225 imageInfo.mipLevels = 1;
2226 imageInfo.arrayLayers = 1;
2227 imageInfo.format = format;
2228 imageInfo.tiling = tiling;
2229 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
2230 imageInfo.usage = usage;
2231 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
2232 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
2233
2234 if (vkCreateImage(window->getDevice(), &imageInfo, nullptr, &image) != VK_SUCCESS) {
2235 throw mxvk::Exception("walk: failed to create raw wall image");
2236 }
2237
2238 VkMemoryRequirements requirements{};
2239 vkGetImageMemoryRequirements(window->getDevice(), image, &requirements);
2240
2241 VkMemoryAllocateInfo allocInfo{};
2242 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
2243 allocInfo.allocationSize = requirements.size;
2244
2245 try {
2246 allocInfo.memoryTypeIndex = findMemoryType(requirements.memoryTypeBits, properties);
2247 if (vkAllocateMemory(window->getDevice(), &allocInfo, nullptr, &memory) != VK_SUCCESS) {
2248 throw mxvk::Exception("walk: failed to allocate raw wall image memory");
2249 }
2250
2251 if (vkBindImageMemory(window->getDevice(), image, memory, 0) != VK_SUCCESS) {
2252 throw mxvk::Exception("walk: failed to bind raw wall image memory");
2253 }
2254 } catch (...) {
2255 if (memory != VK_NULL_HANDLE) {
2256 vkFreeMemory(window->getDevice(), memory, nullptr);
2257 memory = VK_NULL_HANDLE;
2258 }
2259 if (image != VK_NULL_HANDLE) {
2260 vkDestroyImage(window->getDevice(), image, nullptr);
2261 image = VK_NULL_HANDLE;
2262 }
2263 throw;
2264 }
2265 }
2266
2267 VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags) const {
2268 VkImageViewCreateInfo viewInfo{};
2269 viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
2270 viewInfo.image = image;
2271 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
2272 viewInfo.format = format;
2273 viewInfo.subresourceRange.aspectMask = aspectFlags;
2274 viewInfo.subresourceRange.baseMipLevel = 0;
2275 viewInfo.subresourceRange.levelCount = 1;
2276 viewInfo.subresourceRange.baseArrayLayer = 0;
2277 viewInfo.subresourceRange.layerCount = 1;
2278
2279 VkImageView imageView = VK_NULL_HANDLE;
2280 if (vkCreateImageView(window->getDevice(), &viewInfo, nullptr, &imageView) != VK_SUCCESS) {
2281 throw mxvk::Exception("walk: failed to create raw wall image view");
2282 }
2283 return imageView;
2284 }
2285
2286 [[nodiscard]] VkCommandBuffer beginSingleTimeCommands() const {
2287 VkCommandBufferAllocateInfo allocInfo{};
2288 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
2289 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
2290 allocInfo.commandPool = window->getCommandPool();
2291 allocInfo.commandBufferCount = 1;
2292
2293 VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
2294 if (vkAllocateCommandBuffers(window->getDevice(), &allocInfo, &commandBuffer) != VK_SUCCESS) {
2295 throw mxvk::Exception("walk: failed to allocate raw wall command buffer");
2296 }
2297
2298 VkCommandBufferBeginInfo beginInfo{};
2299 beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
2300 beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
2301 if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) {
2302 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
2303 throw mxvk::Exception("walk: failed to begin raw wall command buffer");
2304 }
2305
2306 return commandBuffer;
2307 }
2308
2309 void endSingleTimeCommands(VkCommandBuffer commandBuffer) const {
2310 if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
2311 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
2312 throw mxvk::Exception("walk: failed to end raw wall command buffer");
2313 }
2314
2315 VkSubmitInfo submitInfo{};
2316 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
2317 submitInfo.commandBufferCount = 1;
2318 submitInfo.pCommandBuffers = &commandBuffer;
2319
2320 if (vkQueueSubmit(window->getGraphicsQueue(), 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
2321 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
2322 throw mxvk::Exception("walk: failed to submit raw wall command buffer");
2323 }
2324 if (vkQueueWaitIdle(window->getGraphicsQueue()) != VK_SUCCESS) {
2325 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
2326 throw mxvk::Exception("walk: failed to wait for raw wall upload queue");
2327 }
2328
2329 vkFreeCommandBuffers(window->getDevice(), window->getCommandPool(), 1, &commandBuffer);
2330 }
2331
2332 void transitionImageLayout(VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout) const {
2333 VkCommandBuffer cmd = beginSingleTimeCommands();
2334
2335 VkImageMemoryBarrier barrier{};
2336 barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
2337 barrier.oldLayout = oldLayout;
2338 barrier.newLayout = newLayout;
2339 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2340 barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
2341 barrier.image = image;
2342 barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
2343 barrier.subresourceRange.baseMipLevel = 0;
2344 barrier.subresourceRange.levelCount = 1;
2345 barrier.subresourceRange.baseArrayLayer = 0;
2346 barrier.subresourceRange.layerCount = 1;
2347
2348 VkPipelineStageFlags sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
2349 VkPipelineStageFlags destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
2350 if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
2351 barrier.srcAccessMask = 0;
2352 barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
2353 } else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
2354 barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
2355 barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
2356 sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
2357 destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
2358 }
2359
2360 vkCmdPipelineBarrier(cmd, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
2361 endSingleTimeCommands(cmd);
2362 }
2363
2364 void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) const {
2365 VkCommandBuffer cmd = beginSingleTimeCommands();
2366 VkBufferImageCopy region{};
2367 region.bufferOffset = 0;
2368 region.bufferRowLength = 0;
2369 region.bufferImageHeight = 0;
2370 region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
2371 region.imageSubresource.mipLevel = 0;
2372 region.imageSubresource.baseArrayLayer = 0;
2373 region.imageSubresource.layerCount = 1;
2374 region.imageOffset = {0, 0, 0};
2375 region.imageExtent = {width, height, 1};
2376
2377 vkCmdCopyBufferToImage(cmd, buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
2378 endSingleTimeCommands(cmd);
2379 }
2380
2381 std::vector<char> vertSpv{};
2382 std::vector<char> fragSpv{};
2383
2384 uint32_t vertexCount = 0;
2385 uint32_t indexCount = 0;
2386 VkBuffer vertexBuffer = VK_NULL_HANDLE;
2387 VkDeviceMemory vertexMemory = VK_NULL_HANDLE;
2388 VkBuffer indexBuffer = VK_NULL_HANDLE;
2389 VkDeviceMemory indexMemory = VK_NULL_HANDLE;
2390
2391 VkImage textureImage = VK_NULL_HANDLE;
2392 VkDeviceMemory textureMemory = VK_NULL_HANDLE;
2393 VkImageView textureView = VK_NULL_HANDLE;
2394 VkSampler textureSampler = VK_NULL_HANDLE;
2395
2396 VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
2397 VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
2398 std::vector<VkDescriptorSet> descriptorSets{};
2399
2400 VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
2401 VkPipeline pipeline = VK_NULL_HANDLE;
2402
2403 std::vector<VkBuffer> uniformBuffers{};
2404 std::vector<VkDeviceMemory> uniformBufferMemory{};
2405 std::vector<void *> uniformBuffersMapped{};
2406
2407 VkDevice device [[maybe_unused]] = VK_NULL_HANDLE;
2408 mxvk::VK_Window *window = nullptr;
2409 };
2410
2411 class WalkWindow final : public mxvk::VK_IOWindow {
2412 public:
2414 : mxvk::VK_IOWindow(args.path, "FPS Maze Room - MXVK", args.width, args.height, args.fullscreen, args.enable_vsync),
2415 assetRoot((args.path.empty() || args.path == ".") ? std::string(WALK_ASSET_DIR) : args.path),
2416 shaderRoot(assetRoot + "/data"),
2417 modelRoot(assetRoot + "/data") {
2418 logEnv(std::format("initializing window {}x{} (fullscreen={})", args.width, args.height, args.fullscreen ? "true" : "false"));
2419 logEnv(std::format("asset root: {}", assetRoot));
2420 logEnv(std::format("model root: {}", modelRoot));
2421
2422 std::mt19937 rng(static_cast<uint32_t>(std::chrono::high_resolution_clock::now().time_since_epoch().count()));
2423 world.generate(rng());
2424 logEnv(std::format("world generated (walls={}, pillars={}, collectibles={})",
2425 world.walls().size(),
2426 world.pillars().size(),
2427 world.collectibles().size()));
2428 setClearColor(100.0f / 255.0f, 181.0f / 255.0f, 246.0f / 255.0f, 1.0f);
2429
2430 cameraPos = world.startPosition();
2431 yaw = chooseBestSpawnYaw(cameraPos);
2432 pitch = 0.0f;
2433 updateCameraVectors();
2434
2435 setFont(assetRoot + "/data/font.ttf", 22);
2436
2437 const std::string vertPath = shaderRoot + "/model.vert.spv";
2438 const std::string wallFragPath = shaderRoot + "/wall.frag.spv";
2439 const std::string floorFragPath = shaderRoot + "/floor.frag.spv";
2440 const std::string pillarVertPath = shaderRoot + "/pillar.vert.spv";
2441 const std::string pillarFragPath = shaderRoot + "/pillar.frag.spv";
2442 const std::string objectFragPath = shaderRoot + "/object.frag.spv";
2443 const std::string bulletFragPath = shaderRoot + "/bullet.frag.spv";
2444 const std::string particleFragPath = shaderRoot + "/particle.frag.spv";
2445 const std::string groundTexManifest = assetRoot + "/data/ground.tex";
2446
2447 modelVertSpv = vertPath;
2448 pillarVertSpv = pillarVertPath;
2449 wallFragSpv = wallFragPath;
2450 floorFragSpv = floorFragPath;
2451 pillarFragSpv = pillarFragPath;
2452 objectFragSpv = objectFragPath;
2453 bulletFragSpv = bulletFragPath;
2454
2455 loadModel(floorModel, modelRoot + "/cube.mxmod.z", groundTexManifest, assetRoot + "/data", vertPath, floorFragPath);
2456 loadModel(bulletModel, modelRoot + "/sphere.mxmod.z", "", "", vertPath, bulletFragPath);
2457
2458 logEnv("loading wall renderer assets");
2459 rawWallRenderer.load(this,
2460 groundTexManifest,
2461 assetRoot + "/data",
2462 loadSpv(pillarVertPath),
2463 loadSpv(wallFragPath));
2464 logEnv("wall renderer ready");
2465
2466 logEnv("loading pillar renderer assets");
2467 rawPillarRenderer.load(this,
2468 groundTexManifest,
2469 assetRoot + "/data",
2470 loadSpv(pillarVertPath),
2471 loadSpv(pillarFragPath));
2472 logEnv("pillar renderer ready");
2473
2474 loadModel(saturnModel, assetRoot + "/data/saturn.mxmod.z",
2475 assetRoot + "/data/planet.tex", assetRoot + "/data", vertPath, objectFragPath);
2476 loadModel(birdModel, assetRoot + "/data/tux.obj",
2477 assetRoot + "/data/tux.mtl", assetRoot + "/data", vertPath, objectFragPath);
2478 loadModel(blasterModel, assetRoot + "/data/blaster.obj",
2479 assetRoot + "/data/blaster.mtl", assetRoot + "/data", vertPath, objectFragPath);
2480 normalizeCollectiblesToModel();
2481
2482 pointParticleVertSpv = shaderRoot + "/particle_points.vert.spv";
2483 pointParticleFragSpv = shaderRoot + "/particle_points.frag.spv";
2484 initializePointParticles();
2485 logEnv("point-particle pipeline initialized");
2486
2487 tryOpenFirstGamepad();
2488 SDL_SetWindowRelativeMouseMode(getSDLWindow(), true);
2489 logEnv("mouse capture enabled");
2490 }
2491
2492 ~WalkWindow() override {
2493 logEnv("shutting down walk window");
2494 if (gamepad != nullptr) {
2495 SDL_CloseGamepad(gamepad);
2496 gamepad = nullptr;
2497 gamepadId = 0;
2498 }
2499 if (device != VK_NULL_HANDLE) {
2500 vkDeviceWaitIdle(device);
2501 destroyPointParticles();
2502 cleanupModels();
2503 }
2504 }
2505
2506 void event(SDL_Event &e) override {
2507 const bool is_left_double_click =
2508 (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN &&
2509 e.button.button == SDL_BUTTON_LEFT &&
2510 e.button.clicks >= 2);
2511
2512 if (is_left_double_click) {
2513 if (SDL_Window *const sdlWindow = getSDLWindow(); sdlWindow != nullptr) {
2514 SDL_RaiseWindow(sdlWindow);
2515 SDL_SetWindowMouseGrab(sdlWindow, true);
2516 SDL_SetWindowRelativeMouseMode(sdlWindow, true);
2517 }
2518
2519 mouseCapture = true;
2520 firstMouse = true;
2521 if (!visible()) {
2522 suppressProjectileOnNextLeftDown = true;
2523 }
2524 logEnv("mouse capture enabled (double-click)");
2525 }
2526
2528 }
2529
2530 void console_event(SDL_Event &e) override {
2531 if (e.type == SDL_EVENT_QUIT) {
2532 logEnv("received quit event");
2533 exit();
2534 return;
2535 }
2536
2537 if (e.type == SDL_EVENT_KEY_DOWN) {
2538 if (e.key.key == SDLK_ESCAPE) {
2539 if (mouseCapture) {
2540 mouseCapture = false;
2541 SDL_SetWindowRelativeMouseMode(getSDLWindow(), false);
2542 suppressProjectileOnNextLeftDown = false;
2543 logEnv("mouse capture disabled (ESC)");
2544 } else {
2545 logEnv("exit requested by ESC");
2546 exit();
2547 return;
2548 }
2549 } else if (e.key.key == SDLK_F) {
2550 showFps = !showFps;
2551 logEnv(std::format("FPS overlay {}", showFps ? "enabled" : "disabled"));
2552 }
2553 }
2554
2555 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
2556 logEnv(std::format("gamepad added (id={})", static_cast<int>(e.gdevice.which)));
2557 openGamepad(e.gdevice.which);
2558 }
2559
2560 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
2561 if (gamepad != nullptr && e.gdevice.which == gamepadId) {
2562 logEnv(std::format("gamepad removed (id={})", static_cast<int>(e.gdevice.which)));
2563 SDL_CloseGamepad(gamepad);
2564 gamepad = nullptr;
2565 gamepadId = 0;
2566 }
2567 }
2568
2569 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
2570 if (e.gbutton.button == SDL_GAMEPAD_BUTTON_BACK || e.gbutton.button == SDL_GAMEPAD_BUTTON_START) {
2571 logEnv("exit requested by gamepad back/start");
2572 exit();
2573 } else if (e.gbutton.button == SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER) {
2574 fireProjectile();
2575 } else if (e.gbutton.button == SDL_GAMEPAD_BUTTON_SOUTH && cameraPos.y <= 1.71f) {
2576 jumpVelocity = 0.3f;
2577 logEnv("jump triggered by gamepad");
2578 }
2579 }
2580
2581 if (e.type == SDL_EVENT_MOUSE_MOTION && mouseCapture) {
2582 if (firstMouse) {
2583 firstMouse = false;
2584 return;
2585 }
2586 yaw += static_cast<float>(e.motion.xrel) * mouseSensitivity;
2587 pitch -= static_cast<float>(e.motion.yrel) * mouseSensitivity;
2588 pitch = glm::clamp(pitch, -89.0f, 89.0f);
2589 updateCameraVectors();
2590 }
2591
2592 if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT && mouseCapture) {
2593 if (suppressProjectileOnNextLeftDown) {
2594 suppressProjectileOnNextLeftDown = false;
2595 return;
2596 }
2597 fireProjectile();
2598 }
2599 }
2600
2601 void console_proc() override {
2602 tryOpenFirstGamepad();
2603 const auto now = std::chrono::steady_clock::now();
2604 float deltaTime = std::chrono::duration<float>(now - lastTick).count();
2605 lastTick = now;
2606 deltaTime = std::clamp(deltaTime, 0.0f, 0.05f);
2607
2608 if (!visible()) {
2609 updatePlayer(deltaTime);
2610 }
2611 updateProjectiles(deltaTime);
2612 updateExplosions(deltaTime);
2613 updateCollectibles(deltaTime);
2614
2615 const int aliveObjects = world.activeCollectibles();
2616 if (!visible()) {
2617 printText(std::format("Objects left: {}", aliveObjects), 20, 20, {255, 255, 255, 255});
2618 printText(std::format("Active Bullets: {}", bullets.size()), 20, 48, {255, 220, 120, 255});
2619 if (showFps && deltaTime > 0.0001f) {
2620 const int fps = static_cast<int>(1.0f / deltaTime);
2621 printText(std::format("FPS: {}", fps), 20, 76, {120, 255, 120, 255});
2622 }
2623
2624 const VkExtent2D extent = getSwapchainExtent();
2625 const int cx = static_cast<int>(extent.width / 2U);
2626 const int cy = static_cast<int>(extent.height / 2U);
2627 printText("+", cx - 6, cy - 12, {255, 64, 64, 255});
2628 printText("3D Room - WASD/Left Stick move, Mouse/Right Stick look, Click/RB shoot, Back/Start quit", 20, static_cast<int>(extent.height) - 36, {210, 210, 210, 255});
2629 }
2630 }
2631
2632 void onSwapchainRecreated() override {
2633 logEnv("swapchain recreated; resizing render resources");
2634 floorModel.resize(this);
2635 rawWallRenderer.resize(this);
2636 rawPillarRenderer.resize(this);
2637 saturnModel.resize(this);
2638 birdModel.resize(this);
2639 blasterModel.resize(this);
2640 bulletModel.resize(this);
2641 rebuildPointParticlePipeline();
2642 }
2643
2644 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override {
2645 const VkExtent2D extent = getSwapchainExtent();
2646 const float aspect = (extent.height > 0U)
2647 ? static_cast<float>(extent.width) / static_cast<float>(extent.height)
2648 : 1.0f;
2649
2650 const glm::mat4 view = glm::lookAt(cameraPos, cameraPos + cameraFront, glm::vec3(0.0f, 1.0f, 0.0f));
2651 glm::mat4 proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 1000.0f);
2652 proj[1][1] *= -1.0f;
2653
2654 const float t = static_cast<float>(SDL_GetTicks()) * 0.001f;
2655
2656 // Floor: a thin slab sized to cover the maze footprint.
2657 {
2658 constexpr float floorHalfSize = 100.0f;
2659 constexpr float floorThickness = 0.04f;
2660 const glm::vec3 extent = floorModel.modelAxisExtent();
2661 const glm::vec3 srcScale(
2662 (floorHalfSize * 2.0f) / std::max(extent.x, 1e-4f),
2663 floorThickness / std::max(extent.y, 1e-4f),
2664 (floorHalfSize * 2.0f) / std::max(extent.z, 1e-4f));
2665 glm::mat4 floorWorld = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, -0.02f, 0.0f));
2666 floorWorld = glm::scale(floorWorld, srcScale);
2667 renderModel(cmd, imageIndex, floorModel, floorWorld, view, proj,
2668 glm::vec4(0.0f, 0.0f, 0.0f, t), false);
2669 }
2670
2671 rawWallRenderer.render(cmd,
2672 imageIndex,
2673 world.walls(),
2674 world.wallThickness(),
2675 view,
2676 proj,
2677 glm::vec4(0.58f, 0.58f, 0.65f, t));
2678
2679 rawPillarRenderer.render(cmd, imageIndex, world.pillars(), view, proj, glm::vec4(0.0f, 0.0f, 0.0f, t));
2680
2681 for (const Collectible &obj : world.collectibles()) {
2682 if (!obj.active) {
2683 continue;
2684 }
2685 glm::mat4 world = glm::translate(glm::mat4(1.0f), obj.position);
2686 world = glm::rotate(world, glm::radians(obj.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
2687 world = glm::scale(world, obj.scale);
2688 if (obj.type == Collectible::Type::Saturn) {
2689 renderRawModel(cmd, imageIndex, saturnModel, world, view, proj, glm::vec4(cameraPos, 0.0f));
2690 } else {
2691 renderRawModel(cmd, imageIndex, birdModel, world, view, proj, glm::vec4(cameraPos, 0.0f));
2692 }
2693 }
2694
2695 if (!visible()) {
2696 renderRawModel(cmd,
2697 imageIndex,
2698 blasterModel,
2699 blasterWorldTransform(),
2700 view,
2701 proj,
2702 glm::vec4(cameraPos, 0.0f));
2703 }
2704
2705 for (const Projectile &bullet : bullets) {
2706 if (!bullet.active) {
2707 continue;
2708 }
2709 if (bullet.lifetime < 0.05f) {
2710 continue;
2711 }
2712 glm::mat4 world = glm::translate(glm::mat4(1.0f), bullet.position);
2713 world = glm::scale(world, glm::vec3(0.07f, 0.07f, 0.20f));
2714 const float fadeProgress = glm::clamp(bullet.lifetime / bullet.maxLifetime, 0.0f, 1.0f);
2715 const float distanceProgress = glm::clamp(bullet.distanceTraveled / bullet.maxDistance, 0.0f, 1.0f);
2716 const float alpha = std::min(1.0f - fadeProgress, 1.0f - distanceProgress);
2717 renderRawModel(cmd, imageIndex, bulletModel, world, view, proj, glm::vec4(alpha, 0.0f, 0.0f, 0.0f));
2718 }
2719 renderPointParticles(cmd, view, proj);
2720 }
2721
2722 private:
2723 enum class ProjectileHitType {
2724 None,
2725 Floor,
2726 Wall,
2727 Pillar,
2728 };
2729
2730 struct ProjectileTraceHit {
2731 ProjectileHitType type = ProjectileHitType::None;
2732 glm::vec3 impact{0.0f};
2733 size_t collectibleIndex = 0;
2734 };
2735
2736 [[nodiscard]] ProjectileTraceHit traceProjectileSegment(const glm::vec3 &from, const glm::vec3 &to) const {
2737 const glm::vec3 dir = to - from;
2738 const float travel = glm::length(dir);
2739 if (travel <= 1e-8f) {
2740 return {};
2741 }
2742
2743 constexpr float sampleStride = 0.03f;
2744 constexpr float projectileRadius = 0.015f;
2745 const int steps = std::max(1, static_cast<int>(std::ceil(travel / sampleStride)));
2746 for (int i = 0; i <= steps; ++i) {
2747 const float t = static_cast<float>(i) / static_cast<float>(steps);
2748 const glm::vec3 point = from + (dir * t);
2749 if (pointHitsWall3D(point, projectileRadius)) {
2750 return {ProjectileHitType::Wall, point, 0};
2751 }
2752 if (pointHitsPillar3D(point, projectileRadius)) {
2753 return {ProjectileHitType::Pillar, point, 0};
2754 }
2755 if (point.y <= 0.0f) {
2756 return {ProjectileHitType::Floor, point, 0};
2757 }
2758 }
2759
2760 return {};
2761 }
2762
2763 bool handleConsoleCommand(const std::vector<std::string> &args, std::ostream &out) override {
2764 if (args.empty()) {
2765 return true;
2766 }
2767
2768 const std::string &cmd = args[0];
2769
2770 if (cmd == "spawn_random" || (cmd == "spawn" && args.size() >= 2 && args[1] == "random")) {
2771 const int attempts = (args.size() >= 3 && cmd == "spawn") ? parseIntOrDefault(args[2], 128)
2772 : ((args.size() >= 2 && cmd == "spawn_random") ? parseIntOrDefault(args[1], 128) : 128);
2773 glm::vec3 candidate = cameraPos;
2774 if (!sampleNavigablePoint(1.7f, 0.68f, candidate, std::max(1, attempts))) {
2775 candidate = world.startPosition();
2776 }
2777
2778 cameraPos = candidate;
2779 yaw = chooseBestSpawnYaw(cameraPos);
2780 pitch = 0.0f;
2781 updateCameraVectors();
2782
2783 out << std::format("Spawned at random location ({:.2f}, {:.2f}, {:.2f})", cameraPos.x, cameraPos.y, cameraPos.z);
2784 logEnv("command: spawn_random");
2785 return true;
2786 }
2787
2788 if (cmd == "reset" || cmd == "reset_collectibles") {
2789 std::vector<Collectible> &collectibles = world.collectibles();
2790 for (size_t i = 0; i < collectibles.size(); ++i) {
2791 Collectible &obj = collectibles[i];
2792 obj.active = true;
2793 obj.rotation = glm::vec3(0.0f);
2794 relocateCollectible(i, 2.0f, 128);
2795 }
2796 resolveCollectibleClusters(2.0f, 4);
2797 destroyedCount = 0;
2798 out << std::format("Collectibles reset. Active collectibles: {}", world.activeCollectibles());
2799 logEnv("command: reset collectibles");
2800 return true;
2801 }
2802
2803 if (cmd == "add_collectibles" || cmd == "add_collectables") {
2804 const int requested = (args.size() >= 2) ? parseIntOrDefault(args[1], 10) : 10;
2805 const int toAdd = std::clamp(requested, 1, 200);
2806 std::uniform_int_distribution<int> typeDist(0, 1);
2807 std::uniform_real_distribution<float> saturnScale(0.4f, 0.8f);
2808 std::uniform_real_distribution<float> saturnRotSpeed(5.0f, 15.0f);
2809 std::uniform_real_distribution<float> birdScale(0.3f, 0.5f);
2810 std::uniform_real_distribution<float> birdRotSpeed(20.0f, 60.0f);
2811
2812 int added = 0;
2813 for (int i = 0; i < toAdd; ++i) {
2814 Collectible obj{};
2815 obj.type = (typeDist(rng) == 0) ? Collectible::Type::Saturn : Collectible::Type::Bird;
2816 if (obj.type == Collectible::Type::Saturn) {
2817 const float scale = saturnScale(rng);
2818 obj.scale = glm::vec3(scale);
2819 obj.rotationSpeed = saturnRotSpeed(rng);
2820 obj.radius = saturnHitRadiusForScale(scale);
2821 obj.hitCenterOffset = saturnHitCenterOffsetForScale(scale);
2822 } else {
2823 const float scale = birdScale(rng);
2824 obj.scale = glm::vec3(scale);
2825 obj.rotationSpeed = birdRotSpeed(rng);
2826 obj.radius = birdHitHalfSideForScale(scale);
2827 obj.hitCenterOffset = birdHitCenterOffsetForScale(scale);
2828 }
2829
2830 bool placed = false;
2831 for (int attempt = 0; attempt < 96; ++attempt) {
2832 const float y = (obj.type == Collectible::Type::Bird) ? birdGroundYForScale(obj.scale.x) : 2.5f;
2833 const float placementRadius = placementRadiusForCollectible(obj);
2834 glm::vec3 candidate{};
2835 if (!sampleNavigablePoint(y, placementRadius, candidate, 1)) {
2836 continue;
2837 }
2838
2839 bool overlaps = false;
2840 for (const Collectible &existing : world.collectibles()) {
2841 if (!existing.active) {
2842 continue;
2843 }
2844 const float separation = std::max(5.0f, existing.radius + obj.radius + 0.2f);
2845 if (glm::length(existing.position - candidate) < separation) {
2846 overlaps = true;
2847 break;
2848 }
2849 }
2850
2851 if (!overlaps) {
2852 obj.position = candidate;
2853 placed = true;
2854 break;
2855 }
2856 }
2857
2858 if (placed) {
2859 world.collectibles().push_back(obj);
2860 ++added;
2861 }
2862 }
2863
2864 out << std::format("Added {} collectible(s). Active collectibles: {}",
2865 added,
2866 world.activeCollectibles());
2867 resolveCollectibleClusters(2.0f, 4);
2868 logEnv(std::format("command: add_collectibles requested={} added={}", toAdd, added));
2869 return true;
2870 }
2871
2872 if (cmd == "status") {
2873 out << std::format("pos=({:.2f}, {:.2f}, {:.2f}) yaw={:.2f} pitch={:.2f}\n"
2874 "walls={} pillars={} collectibles(active/total)={}/{} bullets={} particles={} destroyed={}",
2875 cameraPos.x,
2876 cameraPos.y,
2877 cameraPos.z,
2878 yaw,
2879 pitch,
2880 world.walls().size(),
2881 world.pillars().size(),
2882 world.activeCollectibles(),
2883 world.collectibles().size(),
2884 bullets.size(),
2885 explosionParticles.size(),
2886 destroyedCount);
2887 return true;
2888 }
2889
2890 if (cmd == "teleport") {
2891 if (args.size() < 4) {
2892 out << "Usage: teleport <x> <y> <z>";
2893 return true;
2894 }
2895
2896 float x = 0.0f;
2897 float y = 0.0f;
2898 float z = 0.0f;
2899 if (!tryParseFloat(args[1], x) || !tryParseFloat(args[2], y) || !tryParseFloat(args[3], z)) {
2900 out << "teleport: invalid numeric argument(s)";
2901 return true;
2902 }
2903
2904 const glm::vec3 candidate{x, y, z};
2905 if (world.checkWallCollision(candidate, 0.68f) || world.checkPillarCollision(candidate, 0.68f)) {
2906 out << "teleport blocked: target intersects wall/pillar";
2907 return true;
2908 }
2909
2910 cameraPos = candidate;
2911 out << std::format("Teleported to ({:.2f}, {:.2f}, {:.2f})", x, y, z);
2912 logEnv("command: teleport");
2913 return true;
2914 }
2915
2916 if (cmd == "clear_bullets") {
2917 const std::size_t removed = bullets.size();
2918 bullets.clear();
2919 out << std::format("Cleared {} bullet(s)", removed);
2920 return true;
2921 }
2922
2923 if (cmd == "clear_fx") {
2924 const std::size_t removed = explosionParticles.size();
2925 explosionParticles.clear();
2926 out << std::format("Cleared {} particle effect(s)", removed);
2927 return true;
2928 }
2929
2930 if (cmd == "set_fps") {
2931 if (args.size() < 2) {
2932 out << std::format("FPS overlay is currently {}. Usage: set_fps <on|off>", showFps ? "on" : "off");
2933 return true;
2934 }
2935 const std::string value = toLowerCopy(args[1]);
2936 if (value == "on" || value == "1" || value == "true") {
2937 showFps = true;
2938 out << "FPS overlay enabled";
2939 return true;
2940 }
2941 if (value == "off" || value == "0" || value == "false") {
2942 showFps = false;
2943 out << "FPS overlay disabled";
2944 return true;
2945 }
2946
2947 out << "Usage: set_fps <on|off>";
2948 return true;
2949 }
2950
2951 if (cmd == "regen_world") {
2952 const uint32_t seed = (args.size() >= 2) ? static_cast<uint32_t>(parseIntOrDefault(args[1], static_cast<int>(rng())))
2953 : rng();
2954 world.generate(seed);
2955 normalizeCollectiblesToModel();
2956 cameraPos = world.startPosition();
2957 yaw = chooseBestSpawnYaw(cameraPos);
2958 pitch = 0.0f;
2959 updateCameraVectors();
2960 bullets.clear();
2961 explosionParticles.clear();
2962 destroyedCount = 0;
2963
2964 out << std::format("Regenerated world with seed {} (walls={}, pillars={}, collectibles={})",
2965 seed,
2966 world.walls().size(),
2967 world.pillars().size(),
2968 world.collectibles().size());
2969 logEnv(std::format("command: regen_world seed={}", seed));
2970 return true;
2971 }
2972
2973 if (cmd == "set_wall" || cmd == "set_floor" || cmd == "set_pillar" || cmd == "set_object" || cmd == "set_bullet") {
2974 if (args.size() < 2) {
2975 out << std::format("Usage: {} <shader.spv|full/path/to/shader.spv>", cmd);
2976 return true;
2977 }
2978
2979 const std::string shaderPath = resolveShaderPath(args[1]);
2980 std::vector<char> shaderBytes;
2981 try {
2982 shaderBytes = loadSpv(shaderPath);
2983 } catch (const mxvk::Exception &e) {
2984 out << std::format("{}: failed to load shader '{}': {}", cmd, shaderPath, e.text());
2985 return true;
2986 }
2987
2988 if (shaderBytes.empty()) {
2989 out << std::format("{}: shader '{}' is empty", cmd, shaderPath);
2990 return true;
2991 }
2992
2993 if (cmd == "set_wall") {
2994 wallFragSpv = shaderPath;
2995 rawWallRenderer.reloadFragShader(shaderBytes);
2996 out << std::format("Wall shader reloaded from {}", shaderPath);
2997 } else if (cmd == "set_floor") {
2998 floorFragSpv = shaderPath;
2999 floorModel.setShaders(this, modelVertSpv, shaderPath);
3000 out << std::format("Floor shader reloaded from {}", shaderPath);
3001 } else if (cmd == "set_pillar") {
3002 pillarFragSpv = shaderPath;
3003 rawPillarRenderer.reloadFragShader(shaderBytes);
3004 out << std::format("Pillar shader reloaded from {}", shaderPath);
3005 } else if (cmd == "set_object") {
3006 objectFragSpv = shaderPath;
3007 saturnModel.setShaders(this, modelVertSpv, shaderPath);
3008 birdModel.setShaders(this, modelVertSpv, shaderPath);
3009 out << std::format("Object shader reloaded from {}", shaderPath);
3010 } else if (cmd == "set_bullet") {
3011 bulletFragSpv = shaderPath;
3012 bulletModel.setShaders(this, modelVertSpv, shaderPath);
3013 out << std::format("Bullet shader reloaded from {}", shaderPath);
3014 }
3015
3016 logEnv(std::format("command: {} shader={}", cmd, shaderPath));
3017 return true;
3018 }
3019
3020 if (cmd == "list_shaders") {
3021 const std::array<std::pair<std::string_view, std::string>, 11> shaders{{
3022 {"wall.frag", resolveShaderPath("wall.frag.spv")},
3023 {"floor.frag", resolveShaderPath("floor.frag.spv")},
3024 {"pillar.frag", resolveShaderPath("pillar.frag.spv")},
3025 {"object.frag", resolveShaderPath("object.frag.spv")},
3026 {"bullet.frag", resolveShaderPath("bullet.frag.spv")},
3027 {"particle.frag", resolveShaderPath("particle.frag.spv")},
3028 {"particle_points.frag", resolveShaderPath("particle_points.frag.spv")},
3029 {"bubble.frag", resolveShaderPath("bubble.frag.spv")},
3030 {"floor_kale.frag", resolveShaderPath("floor_kale.frag.spv")},
3031 {"floor_swirl.frag", resolveShaderPath("floor_swirl.frag.spv")},
3032 {"floor_twist.frag", resolveShaderPath("floor_twist.frag.spv")},
3033 }};
3034
3035 out << "Available shaders:\n";
3036 for (const auto &[name, path] : shaders) {
3037 out << std::format(" {:<20} {}\n", name, path);
3038 }
3039 out << std::format("Current bindings:\n"
3040 " wall {}\n"
3041 " floor {}\n"
3042 " pillar {}\n"
3043 " object {}\n"
3044 " bullet {}\n",
3045 wallFragSpv,
3046 floorFragSpv,
3047 pillarFragSpv,
3048 objectFragSpv,
3049 bulletFragSpv);
3050 return true;
3051 }
3052
3053 return false;
3054 }
3055
3056 void appendConsoleHelp(std::ostream &out) const override {
3057 out << "\nWalk debug commands:\n"
3058 << " spawn_random [attempts] Spawn player at random valid location\n"
3059 << " spawn random [attempts] Alias for spawn_random\n"
3060 << " reset Reset all collectibles to active\n"
3061 << " add_collectibles [count] Add random collectibles (alias: add_collectables)\n"
3062 << " status Print camera/world/debug state\n"
3063 << " teleport <x> <y> <z> Teleport player if destination is valid\n"
3064 << " clear_bullets Remove all active bullets\n"
3065 << " clear_fx Remove all active explosion particles\n"
3066 << " set_fps <on|off> Toggle FPS overlay\n"
3067 << " set_wall <shader.spv> Reload wall fragment shader\n"
3068 << " set_floor <shader.spv> Reload floor fragment shader\n"
3069 << " set_pillar <shader.spv> Reload pillar fragment shader\n"
3070 << " set_object <shader.spv> Reload object fragment shader\n"
3071 << " set_bullet <shader.spv> Reload bullet fragment shader\n"
3072 << " list_shaders Print available shaders and current bindings\n"
3073 << " regen_world [seed] Regenerate maze, pillars, and collectibles";
3074 }
3075
3076 void logEnv(const std::string &message) {
3077 print(std::format("[walk] {}", message), {255, 100, 255, 255});
3078 }
3079
3080 /// @brief Resolve a shader SPV name to a full path.
3081 ///
3082 /// Looks in the runtime shader directory first; if the file is not
3083 /// found there, the provided @p name is returned as-is so callers can pass
3084 /// absolute paths directly.
3085 [[nodiscard]] std::string resolveShaderPath(const std::string &name) const {
3086 const std::string runtimePath = shaderRoot + "/" + name;
3087 if (std::filesystem::exists(runtimePath)) {
3088 return runtimePath;
3089 }
3090 return name;
3091 }
3092
3093 [[nodiscard]] static std::string toLowerCopy(std::string value) {
3094 std::transform(value.begin(), value.end(), value.begin(), [](const unsigned char ch) {
3095 return static_cast<char>(std::tolower(ch));
3096 });
3097 return value;
3098 }
3099
3100 [[nodiscard]] static int parseIntOrDefault(const std::string &text, const int fallback) {
3101 int value = fallback;
3102 const auto begin = text.data();
3103 const auto end = text.data() + text.size();
3104 const auto [ptr, ec] = std::from_chars(begin, end, value);
3105 if (ec != std::errc{} || ptr != end) {
3106 return fallback;
3107 }
3108 return value;
3109 }
3110
3111 [[nodiscard]] static bool tryParseFloat(const std::string &text, float &outValue) {
3112 try {
3113 size_t parsed = 0;
3114 const float value = std::stof(text, &parsed);
3115 if (parsed != text.size()) {
3116 return false;
3117 }
3118 outValue = value;
3119 return true;
3120 } catch (...) {
3121 return false;
3122 }
3123 }
3124
3125 bool sampleNavigablePoint(const float y, const float radius, glm::vec3 &outPoint, const int maxAttempts) {
3126 float minX = -50.0f;
3127 float maxX = 50.0f;
3128 float minZ = -50.0f;
3129 float maxZ = 50.0f;
3130
3131 bool haveBounds = false;
3132 for (const WallSegment &wall : world.walls()) {
3133 if (!haveBounds) {
3134 minX = std::min(wall.start.x, wall.end.x);
3135 maxX = std::max(wall.start.x, wall.end.x);
3136 minZ = std::min(wall.start.z, wall.end.z);
3137 maxZ = std::max(wall.start.z, wall.end.z);
3138 haveBounds = true;
3139 } else {
3140 minX = std::min(minX, std::min(wall.start.x, wall.end.x));
3141 maxX = std::max(maxX, std::max(wall.start.x, wall.end.x));
3142 minZ = std::min(minZ, std::min(wall.start.z, wall.end.z));
3143 maxZ = std::max(maxZ, std::max(wall.start.z, wall.end.z));
3144 }
3145 }
3146
3147 if (!haveBounds) {
3148 outPoint = world.startPosition();
3149 outPoint.y = y;
3150 return true;
3151 }
3152
3153 const float margin = std::max(0.8f, radius + 0.5f);
3154 minX += margin;
3155 maxX -= margin;
3156 minZ += margin;
3157 maxZ -= margin;
3158
3159 if (minX > maxX || minZ > maxZ) {
3160 outPoint = world.startPosition();
3161 outPoint.y = y;
3162 return true;
3163 }
3164
3165 std::uniform_real_distribution<float> distX(minX, maxX);
3166 std::uniform_real_distribution<float> distZ(minZ, maxZ);
3167 for (int i = 0; i < maxAttempts; ++i) {
3168 const glm::vec3 candidate{distX(rng), y, distZ(rng)};
3169 if (!world.checkWallCollision(candidate, radius) && !world.checkPillarCollision(candidate, radius)) {
3170 outPoint = candidate;
3171 return true;
3172 }
3173 }
3174
3175 return false;
3176 }
3177
3178 [[nodiscard]] static const char *collectibleTypeName(Collectible::Type type) noexcept {
3179 return type == Collectible::Type::Saturn ? "saturn" : "bird";
3180 }
3181
3182 void loadModel(mxvk::VKAbstractModel &model,
3183 const std::string &modelPath,
3184 const std::string &textureManifest,
3185 const std::string &textureBase,
3186 const std::string &vertSpv,
3187 const std::string &fragSpv,
3188 bool backfaceCulling = false) {
3189 logEnv(std::format("loading model '{}'", modelPath));
3190 model.load(this, modelPath, textureManifest, textureBase, 1.0f);
3191 model.setBackfaceCulling(backfaceCulling);
3192 model.setShaders(this, vertSpv, fragSpv);
3193 logEnv(std::format("model ready '{}'", modelPath));
3194 }
3195
3196 void cleanupModels() {
3197 floorModel.cleanup(this);
3198 rawWallRenderer.cleanup(this);
3199 rawPillarRenderer.cleanup(this);
3200
3201 saturnModel.cleanup(this);
3202 birdModel.cleanup(this);
3203 blasterModel.cleanup(this);
3204 bulletModel.cleanup(this);
3205 }
3206
3207 [[nodiscard]] uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) const {
3208 VkPhysicalDeviceMemoryProperties memProperties{};
3209 vkGetPhysicalDeviceMemoryProperties(getPhysicalDevice(), &memProperties);
3210 for (uint32_t i = 0; i < memProperties.memoryTypeCount; ++i) {
3211 if ((typeFilter & (1u << i)) != 0u && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
3212 return i;
3213 }
3214 }
3215 throw mxvk::Exception("walk: failed to find suitable Vulkan memory type for point particles");
3216 }
3217
3218 void initializePointParticles() {
3219 if (!ensureRenderResources()) {
3220 throw mxvk::Exception("walk: render resources unavailable for point particles");
3221 }
3222
3223 destroyPointParticles();
3224 try {
3225 VkBufferCreateInfo bufferInfo{};
3226 bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
3227 bufferInfo.size = maxPointVertices * sizeof(ParticlePointVertex);
3228 bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
3229 bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
3230 if (vkCreateBuffer(getDevice(), &bufferInfo, nullptr, &pointVertexBuffer) != VK_SUCCESS) {
3231 throw mxvk::Exception("walk: failed to create point particle vertex buffer");
3232 }
3233
3234 VkMemoryRequirements memReq{};
3235 vkGetBufferMemoryRequirements(getDevice(), pointVertexBuffer, &memReq);
3236 VkMemoryAllocateInfo allocInfo{};
3237 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
3238 allocInfo.allocationSize = memReq.size;
3239 allocInfo.memoryTypeIndex = findMemoryType(memReq.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
3240 if (vkAllocateMemory(getDevice(), &allocInfo, nullptr, &pointVertexMemory) != VK_SUCCESS) {
3241 throw mxvk::Exception("walk: failed to allocate point particle vertex memory");
3242 }
3243 if (vkBindBufferMemory(getDevice(), pointVertexBuffer, pointVertexMemory, 0) != VK_SUCCESS) {
3244 throw mxvk::Exception("walk: failed to bind point particle vertex memory");
3245 }
3246 if (vkMapMemory(getDevice(), pointVertexMemory, 0, bufferInfo.size, 0, &pointVertexMapped) != VK_SUCCESS) {
3247 throw mxvk::Exception("walk: failed to map point particle vertex memory");
3248 }
3249
3250 rebuildPointParticlePipeline();
3251 } catch (...) {
3252 destroyPointParticles();
3253 throw;
3254 }
3255 }
3256
3257 void rebuildPointParticlePipeline() {
3258 if (pointPipeline != VK_NULL_HANDLE) {
3259 vkDestroyPipeline(getDevice(), pointPipeline, nullptr);
3260 pointPipeline = VK_NULL_HANDLE;
3261 }
3262 if (pointPipelineLayout != VK_NULL_HANDLE) {
3263 vkDestroyPipelineLayout(getDevice(), pointPipelineLayout, nullptr);
3264 pointPipelineLayout = VK_NULL_HANDLE;
3265 }
3266
3267 if (getSwapchainFormat() == VK_FORMAT_UNDEFINED) {
3268 return;
3269 }
3270
3271 const std::vector<char> vertBytes = loadSpv(pointParticleVertSpv);
3272 const std::vector<char> fragBytes = loadSpv(pointParticleFragSpv);
3273 const VkShaderModule vertModule = mxvk::create_shader_module(getDevice(), vertBytes);
3274 const VkShaderModule fragModule = mxvk::create_shader_module(getDevice(), fragBytes);
3275
3276 VkPipelineShaderStageCreateInfo vertStage{};
3277 vertStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
3278 vertStage.stage = VK_SHADER_STAGE_VERTEX_BIT;
3279 vertStage.module = vertModule;
3280 vertStage.pName = "main";
3281
3282 VkPipelineShaderStageCreateInfo fragStage{};
3283 fragStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
3284 fragStage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
3285 fragStage.module = fragModule;
3286 fragStage.pName = "main";
3287 const std::array<VkPipelineShaderStageCreateInfo, 2> stages = {vertStage, fragStage};
3288
3289 VkVertexInputBindingDescription binding{};
3290 binding.binding = 0;
3291 binding.stride = sizeof(ParticlePointVertex);
3292 binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
3293
3294 std::array<VkVertexInputAttributeDescription, 3> attrs{};
3295 attrs[0] = {0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(ParticlePointVertex, pos)};
3296 attrs[1] = {1, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(ParticlePointVertex, color)};
3297 attrs[2] = {2, 0, VK_FORMAT_R32_SFLOAT, offsetof(ParticlePointVertex, size)};
3298
3299 VkPipelineVertexInputStateCreateInfo vertexInput{};
3300 vertexInput.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
3301 vertexInput.vertexBindingDescriptionCount = 1;
3302 vertexInput.pVertexBindingDescriptions = &binding;
3303 vertexInput.vertexAttributeDescriptionCount = static_cast<uint32_t>(attrs.size());
3304 vertexInput.pVertexAttributeDescriptions = attrs.data();
3305
3306 VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
3307 inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
3308 inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST;
3309
3310 const std::array<VkDynamicState, 2> dynamicStates = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
3311 VkPipelineDynamicStateCreateInfo dynamicInfo{};
3312 dynamicInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
3313 dynamicInfo.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size());
3314 dynamicInfo.pDynamicStates = dynamicStates.data();
3315
3316 VkPipelineViewportStateCreateInfo viewportState{};
3317 viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
3318 viewportState.viewportCount = 1;
3319 viewportState.scissorCount = 1;
3320
3321 VkPipelineRasterizationStateCreateInfo rasterizer{};
3322 rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
3323 rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
3324 rasterizer.cullMode = VK_CULL_MODE_NONE;
3325 rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
3326 rasterizer.lineWidth = 1.0f;
3327
3328 VkPipelineMultisampleStateCreateInfo multisample{};
3329 multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
3330 multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
3331
3332 VkPipelineDepthStencilStateCreateInfo depthStencil{};
3333 depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
3334 depthStencil.depthTestEnable = VK_FALSE;
3335 depthStencil.depthWriteEnable = VK_FALSE;
3336 depthStencil.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
3337
3338 VkPipelineColorBlendAttachmentState blendAttachment{};
3339 blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
3340 blendAttachment.blendEnable = VK_TRUE;
3341 blendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
3342 blendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE;
3343 blendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
3344 blendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
3345 blendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
3346 blendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
3347
3348 VkPipelineColorBlendStateCreateInfo colorBlend{};
3349 colorBlend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
3350 colorBlend.attachmentCount = 1;
3351 colorBlend.pAttachments = &blendAttachment;
3352
3353 VkPushConstantRange pushRange{};
3354 pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
3355 pushRange.offset = 0;
3356 pushRange.size = sizeof(glm::mat4);
3357
3358 VkPipelineLayoutCreateInfo layoutInfo{};
3359 layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
3360 layoutInfo.pushConstantRangeCount = 1;
3361 layoutInfo.pPushConstantRanges = &pushRange;
3362 if (vkCreatePipelineLayout(getDevice(), &layoutInfo, nullptr, &pointPipelineLayout) != VK_SUCCESS) {
3363 vkDestroyShaderModule(getDevice(), fragModule, nullptr);
3364 vkDestroyShaderModule(getDevice(), vertModule, nullptr);
3365 throw mxvk::Exception("walk: failed to create point particle pipeline layout");
3366 }
3367
3368 const VkFormat colorFormat = getSwapchainFormat();
3369 const VkFormat depthFormat = getDepthFormat();
3370 VkPipelineRenderingCreateInfo renderingInfo{};
3371 renderingInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO;
3372 renderingInfo.colorAttachmentCount = 1;
3373 renderingInfo.pColorAttachmentFormats = &colorFormat;
3374 if (depthFormat != VK_FORMAT_UNDEFINED) {
3375 renderingInfo.depthAttachmentFormat = depthFormat;
3376 }
3377
3378 VkGraphicsPipelineCreateInfo pipelineInfo{};
3379 pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
3380 pipelineInfo.pNext = &renderingInfo;
3381 pipelineInfo.stageCount = static_cast<uint32_t>(stages.size());
3382 pipelineInfo.pStages = stages.data();
3383 pipelineInfo.pVertexInputState = &vertexInput;
3384 pipelineInfo.pInputAssemblyState = &inputAssembly;
3385 pipelineInfo.pViewportState = &viewportState;
3386 pipelineInfo.pRasterizationState = &rasterizer;
3387 pipelineInfo.pMultisampleState = &multisample;
3388 pipelineInfo.pDepthStencilState = &depthStencil;
3389 pipelineInfo.pColorBlendState = &colorBlend;
3390 pipelineInfo.pDynamicState = &dynamicInfo;
3391 pipelineInfo.layout = pointPipelineLayout;
3392 pipelineInfo.renderPass = VK_NULL_HANDLE;
3393
3394 if (vkCreateGraphicsPipelines(getDevice(), VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pointPipeline) != VK_SUCCESS) {
3395 vkDestroyShaderModule(getDevice(), fragModule, nullptr);
3396 vkDestroyShaderModule(getDevice(), vertModule, nullptr);
3397 throw mxvk::Exception("walk: failed to create point particle graphics pipeline");
3398 }
3399
3400 vkDestroyShaderModule(getDevice(), fragModule, nullptr);
3401 vkDestroyShaderModule(getDevice(), vertModule, nullptr);
3402 }
3403
3404 void destroyPointParticles() {
3405 if (pointPipeline != VK_NULL_HANDLE) {
3406 vkDestroyPipeline(getDevice(), pointPipeline, nullptr);
3407 pointPipeline = VK_NULL_HANDLE;
3408 }
3409 if (pointPipelineLayout != VK_NULL_HANDLE) {
3410 vkDestroyPipelineLayout(getDevice(), pointPipelineLayout, nullptr);
3411 pointPipelineLayout = VK_NULL_HANDLE;
3412 }
3413 if (pointVertexMapped != nullptr) {
3414 vkUnmapMemory(getDevice(), pointVertexMemory);
3415 pointVertexMapped = nullptr;
3416 }
3417 if (pointVertexBuffer != VK_NULL_HANDLE) {
3418 vkDestroyBuffer(getDevice(), pointVertexBuffer, nullptr);
3419 pointVertexBuffer = VK_NULL_HANDLE;
3420 }
3421 if (pointVertexMemory != VK_NULL_HANDLE) {
3422 vkFreeMemory(getDevice(), pointVertexMemory, nullptr);
3423 pointVertexMemory = VK_NULL_HANDLE;
3424 }
3425 }
3426
3427 void renderPointParticles(VkCommandBuffer cmd, const glm::mat4 &view, const glm::mat4 &proj) {
3428 if (pointPipeline == VK_NULL_HANDLE || pointPipelineLayout == VK_NULL_HANDLE || pointVertexMapped == nullptr) {
3429 return;
3430 }
3431
3432 std::vector<ParticlePointVertex> vertices{};
3433 vertices.reserve(2048);
3434
3435 for (const Projectile &bullet : bullets) {
3436 if (!bullet.active) {
3437 continue;
3438 }
3439 for (const Projectile::TrailPoint &point : bullet.trail) {
3440 const float life = glm::clamp(point.lifetime / point.maxLifetime, 0.0f, 1.0f);
3441 const float fade = 1.0f - life;
3442 vertices.push_back({point.position, glm::vec4(1.0f, 0.2f, 0.0f, fade * 0.8f), 12.0f});
3443 }
3444 }
3445
3446 for (const ExplosionParticle &particle : explosionParticles) {
3447 if (!particle.active) {
3448 continue;
3449 }
3450 const float life = glm::clamp(particle.lifetime / particle.maxLifetime, 0.0f, 1.0f);
3451 const float fade = 1.0f - life;
3452 const float sizePx = glm::clamp(particle.size * 320.0f, 12.0f, 160.0f);
3453 vertices.push_back({particle.position, glm::vec4(particle.color, fade), sizePx});
3454 }
3455
3456 if (vertices.empty()) {
3457 return;
3458 }
3459
3460 if (vertices.size() > maxPointVertices) {
3461 vertices.resize(maxPointVertices);
3462 }
3463 std::memcpy(pointVertexMapped, vertices.data(), vertices.size() * sizeof(ParticlePointVertex));
3464
3465 const VkBuffer vb = pointVertexBuffer;
3466 const VkDeviceSize offset = 0;
3467 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pointPipeline);
3468 vkCmdBindVertexBuffers(cmd, 0, 1, &vb, &offset);
3469 const glm::mat4 vp = proj * view;
3470 vkCmdPushConstants(cmd, pointPipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(glm::mat4), &vp);
3471 vkCmdDraw(cmd, static_cast<uint32_t>(vertices.size()), 1, 0, 0);
3472 }
3473
3474 bool openGamepad(SDL_JoystickID id) {
3475 if (gamepad != nullptr && gamepadId == id) {
3476 return true;
3477 }
3478 if (gamepad != nullptr) {
3479 SDL_CloseGamepad(gamepad);
3480 gamepad = nullptr;
3481 gamepadId = 0;
3482 }
3483 gamepad = SDL_OpenGamepad(id);
3484 if (gamepad == nullptr) {
3485 logEnv(std::format("failed to open gamepad id={}", static_cast<int>(id)));
3486 return false;
3487 }
3488 gamepadId = id;
3489 const char *padName = SDL_GetGamepadName(gamepad);
3490 logEnv(std::format("gamepad connected: id={} name='{}'",
3491 static_cast<int>(id),
3492 padName != nullptr ? padName : "unknown"));
3493 return true;
3494 }
3495
3496 void tryOpenFirstGamepad() {
3497 if (gamepad != nullptr) {
3498 return;
3499 }
3500 int count = 0;
3501 SDL_JoystickID *ids = SDL_GetGamepads(&count);
3502 if (ids == nullptr || count <= 0) {
3503 if (ids != nullptr) {
3504 SDL_free(ids);
3505 }
3506 return;
3507 }
3508 openGamepad(ids[0]);
3509 SDL_free(ids);
3510 }
3511
3512 [[nodiscard]] static glm::mat4 composeNormalizedModel(const mxvk::VKAbstractModel &model, const glm::mat4 &world) {
3513 glm::mat4 transform = world;
3514 transform = transform * glm::scale(glm::mat4(1.0f), glm::vec3(model.modelRenderScale()));
3515 transform = transform * glm::translate(glm::mat4(1.0f), model.modelCenterOffset());
3516 return transform;
3517 }
3518
3519 // For meshes whose final world-space dimensions are already baked into `world`
3520 // (walls/pillars/floor) we still want to recenter the source mesh on its
3521 // bounding-box center, but we must NOT compound the renderScale on top.
3522 [[nodiscard]] static glm::mat4 composeRecenteredModel(const mxvk::VKAbstractModel &model, const glm::mat4 &world) {
3523 return world * glm::translate(glm::mat4(1.0f), model.modelCenterOffset());
3524 }
3525
3526 void renderModel(VkCommandBuffer cmd,
3527 uint32_t imageIndex,
3528 mxvk::VKAbstractModel &model,
3529 const glm::mat4 &world,
3530 const glm::mat4 &view,
3531 const glm::mat4 &proj,
3532 const glm::vec4 &fx,
3533 bool autoNormalize = true) {
3534 mxvk::UniformBufferObject ubo{};
3535 ubo.model = autoNormalize ? composeNormalizedModel(model, world)
3536 : composeRecenteredModel(model, world);
3537 ubo.view = view;
3538 ubo.proj = proj;
3539 ubo.fx = fx;
3540 model.updateUBO(imageIndex, ubo);
3541 model.render(cmd, imageIndex, false);
3542 }
3543
3544 void renderRawModel(VkCommandBuffer cmd,
3545 uint32_t imageIndex,
3546 mxvk::VKAbstractModel &model,
3547 const glm::mat4 &world,
3548 const glm::mat4 &view,
3549 const glm::mat4 &proj,
3550 const glm::vec4 &fx) {
3551 mxvk::UniformBufferObject ubo{};
3552 ubo.model = world;
3553 ubo.view = view;
3554 ubo.proj = proj;
3555 ubo.fx = fx;
3556 model.updateUBO(imageIndex, ubo);
3557 model.render(cmd, imageIndex, false);
3558 }
3559
3560 void updateCameraVectors() {
3561 glm::vec3 front(0.0f);
3562 front.x = std::cos(glm::radians(yaw)) * std::cos(glm::radians(pitch));
3563 front.y = std::sin(glm::radians(pitch));
3564 front.z = std::sin(glm::radians(yaw)) * std::cos(glm::radians(pitch));
3565 cameraFront = glm::normalize(front);
3566 }
3567
3568 void buildCameraBasis(glm::vec3 &forward, glm::vec3 &right, glm::vec3 &up) const {
3569 forward = cameraFront;
3570 if (glm::length(forward) <= 1e-5f) {
3571 forward = glm::vec3(0.0f, 0.0f, -1.0f);
3572 } else {
3573 forward = glm::normalize(forward);
3574 }
3575
3576 right = glm::cross(forward, glm::vec3(0.0f, 1.0f, 0.0f));
3577 if (glm::length(right) <= 1e-5f) {
3578 right = glm::vec3(1.0f, 0.0f, 0.0f);
3579 } else {
3580 right = glm::normalize(right);
3581 }
3582
3583 up = glm::cross(right, forward);
3584 if (glm::length(up) <= 1e-5f) {
3585 up = glm::vec3(0.0f, 1.0f, 0.0f);
3586 } else {
3587 up = glm::normalize(up);
3588 }
3589 }
3590
3591 [[nodiscard]] glm::vec3 blasterMuzzleTipPosition() const {
3592 glm::vec3 forward(0.0f);
3593 glm::vec3 right(0.0f);
3594 glm::vec3 up(0.0f);
3595 buildCameraBasis(forward, right, up);
3596 return cameraPos + (forward * 0.55f) + (right * 0.18f) - (up * 0.12f);
3597 }
3598
3599 [[nodiscard]] glm::vec3 projectileSpawnPosition() const {
3600 glm::vec3 forward(0.0f);
3601 glm::vec3 right(0.0f);
3602 glm::vec3 up(0.0f);
3603 buildCameraBasis(forward, right, up);
3604 constexpr float projectileForwardOffset = 0.015f;
3605 return blasterMuzzleTipPosition() + (forward * projectileForwardOffset);
3606 }
3607
3608 [[nodiscard]] glm::mat4 blasterWorldTransform() const {
3609 glm::vec3 forward(0.0f);
3610 glm::vec3 right(0.0f);
3611 glm::vec3 up(0.0f);
3612 buildCameraBasis(forward, right, up);
3613
3614 constexpr float blasterScale = 0.45f;
3615 constexpr glm::vec3 localMuzzle(0.95f, 0.09f, 0.0f);
3616 const glm::vec3 desiredMuzzle = blasterMuzzleTipPosition();
3617 const glm::vec3 origin = desiredMuzzle - (forward * (localMuzzle.x * blasterScale)) - (up * (localMuzzle.y * blasterScale)) - (right * (localMuzzle.z * blasterScale));
3618
3619 glm::mat4 world(1.0f);
3620 world[0] = glm::vec4(forward * blasterScale, 0.0f);
3621 world[1] = glm::vec4(up * blasterScale, 0.0f);
3622 world[2] = glm::vec4(right * blasterScale, 0.0f);
3623 world[3] = glm::vec4(origin, 1.0f);
3624 return world;
3625 }
3626
3627 [[nodiscard]] float viewDistanceInDirection(const glm::vec3 &origin, const glm::vec3 &direction) const {
3628 const glm::vec3 dir = glm::normalize(glm::vec3(direction.x, 0.0f, direction.z));
3629 constexpr float maxDistance = 14.0f;
3630 constexpr float step = 0.35f;
3631 constexpr float probeRadius = 0.30f;
3632 for (float d = step; d <= maxDistance; d += step) {
3633 const glm::vec3 point = origin + (dir * d);
3634 if (world.checkWallCollision(point, probeRadius) || world.checkPillarCollision(point, probeRadius)) {
3635 return d - step;
3636 }
3637 }
3638 return maxDistance;
3639 }
3640
3641 [[nodiscard]] float chooseBestSpawnYaw(const glm::vec3 &origin) const {
3642 constexpr float pi = 3.14159265358979323846f;
3643 constexpr int sampleCount = 48;
3644 float bestDistance = -1.0f;
3645 float bestYaw = yaw;
3646 for (int i = 0; i < sampleCount; ++i) {
3647 const float angle = (-pi) + (2.0f * pi * static_cast<float>(i) / static_cast<float>(sampleCount));
3648 const glm::vec3 dir(std::cos(angle), 0.0f, std::sin(angle));
3649 const float dist = viewDistanceInDirection(origin, dir);
3650 if (dist > bestDistance) {
3651 bestDistance = dist;
3652 bestYaw = glm::degrees(angle);
3653 }
3654 }
3655 return bestYaw;
3656 }
3657
3658 void updatePlayer(float deltaTime) {
3659 const bool *keys = SDL_GetKeyboardState(nullptr);
3660 glm::vec3 horizontalFront = glm::normalize(glm::vec3(cameraFront.x, 0.0f, cameraFront.z));
3661 if (glm::length(horizontalFront) < 0.0001f) {
3662 horizontalFront = glm::vec3(0.0f, 0.0f, -1.0f);
3663 }
3664 const glm::vec3 right = glm::normalize(glm::cross(horizontalFront, glm::vec3(0.0f, 1.0f, 0.0f)));
3665
3666 glm::vec3 desired = cameraPos;
3667 bool sprint = keys[SDL_SCANCODE_LSHIFT] != 0;
3668 if (gamepad != nullptr && SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_LEFT_STICK)) {
3669 sprint = true;
3670 }
3671 const float cameraSpeed = 0.2f;
3672 const float speed = sprint ? cameraSpeed * 2.0f : cameraSpeed;
3673 const float frameScale = deltaTime * 60.0f;
3674 const float moveStep = speed * frameScale;
3675
3676 if (keys[SDL_SCANCODE_W]) {
3677 desired += horizontalFront * moveStep;
3678 }
3679 if (keys[SDL_SCANCODE_S]) {
3680 desired -= horizontalFront * moveStep;
3681 }
3682 if (keys[SDL_SCANCODE_A]) {
3683 desired -= right * moveStep;
3684 }
3685 if (keys[SDL_SCANCODE_D]) {
3686 desired += right * moveStep;
3687 }
3688
3689 if (gamepad != nullptr) {
3690 const Sint16 leftX = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
3691 const Sint16 leftY = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
3692 if (std::abs(leftX) > stickDeadZone) {
3693 desired += moveStep * (static_cast<float>(leftX) / 32768.0f) * right;
3694 }
3695 if (std::abs(leftY) > stickDeadZone) {
3696 desired -= moveStep * (static_cast<float>(leftY) / 32768.0f) * horizontalFront;
3697 }
3698
3699 const Sint16 rightX = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX);
3700 const Sint16 rightY = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY);
3701 if (std::abs(rightX) > stickDeadZone || std::abs(rightY) > stickDeadZone) {
3702 yaw += (static_cast<float>(rightX) / 32768.0f) * controllerLookSensitivity;
3703 pitch -= (static_cast<float>(rightY) / 32768.0f) * controllerLookSensitivity;
3704 pitch = glm::clamp(pitch, -89.0f, 89.0f);
3705 updateCameraVectors();
3706 }
3707 }
3708
3709 constexpr float playerRadius = 0.5f;
3710 constexpr float cameraStandOff = 0.18f;
3711 const float collisionRadius = playerRadius + cameraStandOff;
3712 const auto isBlocked = [this, collisionRadius](const glm::vec3 &position) {
3713 return world.checkWallCollision(position, collisionRadius) || world.checkPillarCollision(position, collisionRadius);
3714 };
3715
3716 if (!isBlocked(desired)) {
3717 cameraPos = desired;
3718 } else {
3719 // Resolve per-axis so the player slides along obstacles instead of
3720 // clipping into them or fully stopping on diagonal movement.
3721 glm::vec3 tryX = cameraPos;
3722 tryX.x = desired.x;
3723 if (!isBlocked(tryX)) {
3724 cameraPos.x = tryX.x;
3725 }
3726
3727 glm::vec3 tryZ = cameraPos;
3728 tryZ.z = desired.z;
3729 if (!isBlocked(tryZ)) {
3730 cameraPos.z = tryZ.z;
3731 }
3732 }
3733
3734 const bool crouch = keys[SDL_SCANCODE_LCTRL] != 0;
3735 const float minHeight = crouch ? 0.8f : 1.7f;
3736 if (keys[SDL_SCANCODE_SPACE] && cameraPos.y <= minHeight + 0.01f) {
3737 jumpVelocity = 0.3f;
3738 }
3739
3740 cameraPos.y += jumpVelocity * deltaTime * 60.0f;
3741 jumpVelocity -= gravity * deltaTime * 60.0f;
3742 if (cameraPos.y < minHeight) {
3743 cameraPos.y = minHeight;
3744 jumpVelocity = 0.0f;
3745 }
3746 }
3747
3748 void fireProjectile() {
3749 emitMuzzleParticles();
3750
3751 Projectile bullet{};
3752 bullet.position = projectileSpawnPosition();
3753 bullet.direction = glm::normalize(cameraFront);
3754 bullets.push_back(bullet);
3755 logEnv(std::format("projectile fired from ({:.2f}, {:.2f}, {:.2f}) dir=({:.2f}, {:.2f}, {:.2f}) active_bullets={}",
3756 bullet.position.x,
3757 bullet.position.y,
3758 bullet.position.z,
3759 bullet.direction.x,
3760 bullet.direction.y,
3761 bullet.direction.z,
3762 bullets.size()));
3763 }
3764
3765 void emitMuzzleParticles() {
3766 glm::vec3 forward(0.0f);
3767 glm::vec3 right(0.0f);
3768 glm::vec3 up(0.0f);
3769 buildCameraBasis(forward, right, up);
3770
3771 const glm::vec3 muzzle = blasterMuzzleTipPosition();
3772 std::uniform_real_distribution<float> lateralJitter(-0.20f, 0.20f);
3773 std::uniform_real_distribution<float> verticalJitter(-0.12f, 0.12f);
3774 std::uniform_real_distribution<float> speedDist(8.0f, 26.0f);
3775 std::uniform_real_distribution<float> lifeDist(0.06f, 0.16f);
3776 std::uniform_real_distribution<float> warmDist(0.75f, 1.0f);
3777
3778 constexpr int particleCount = 24;
3779 for (int i = 0; i < particleCount; ++i) {
3780 ExplosionParticle p{};
3781 p.position = muzzle + (forward * 0.01f);
3782
3783 glm::vec3 dir = forward + (right * lateralJitter(rng)) + (up * verticalJitter(rng));
3784 if (glm::length(dir) <= 1e-5f) {
3785 dir = forward;
3786 } else {
3787 dir = glm::normalize(dir);
3788 }
3789
3790 const float speed = speedDist(rng);
3791 p.velocity = dir * speed;
3792 p.color = glm::vec3(warmDist(rng), warmDist(rng) * 0.7f, warmDist(rng) * 0.18f);
3793 p.maxLifetime = lifeDist(rng);
3794 p.size = 0.035f + (speed * 0.003f);
3795 explosionParticles.push_back(p);
3796 }
3797 }
3798
3799 void updateProjectiles(float deltaTime) {
3800 for (size_t bulletIndex = 0; bulletIndex < bullets.size(); ++bulletIndex) {
3801 Projectile &bullet = bullets[bulletIndex];
3802 if (!bullet.active) {
3803 continue;
3804 }
3805
3806 const glm::vec3 previous = bullet.position;
3807 const glm::vec3 displacement = bullet.direction * bullet.speed * deltaTime;
3808 bullet.position += displacement;
3809 bullet.lifetime += deltaTime;
3810 bullet.distanceTraveled += glm::length(displacement);
3811 bullet.trailTimer += deltaTime;
3812 if (bullet.trailTimer >= 0.02f) {
3813 Projectile::TrailPoint point{};
3814 point.position = bullet.position;
3815 bullet.trail.push_back(point);
3816 bullet.trailTimer = 0.0f;
3817 }
3818 for (Projectile::TrailPoint &point : bullet.trail) {
3819 point.lifetime += deltaTime;
3820 }
3821 bullet.trail.erase(
3822 std::remove_if(bullet.trail.begin(), bullet.trail.end(), [](const Projectile::TrailPoint &point) {
3823 return point.lifetime >= point.maxLifetime;
3824 }),
3825 bullet.trail.end());
3826
3827 size_t collectibleIndex = 0;
3828 glm::vec3 collectibleImpact{0.0f};
3829 if (lineHitCollectible(previous, bullet.position, collectibleIndex, collectibleImpact)) {
3830 createExplosion(collectibleImpact, 5000, false);
3831 const Collectible::Type hitType = world.collectibles()[collectibleIndex].type;
3832 const bool removed = deactivateCollectibleAt(collectibleIndex);
3833 resolveCollectibleClusters(2.0f, 3);
3834 bullet.active = false;
3835 if (removed) {
3836 ++destroyedCount;
3837 }
3838 logEnv(std::format("bullet {} hit {} collectible {} at ({:.2f}, {:.2f}, {:.2f}); destroyed={}",
3839 bulletIndex,
3840 collectibleTypeName(hitType),
3841 collectibleIndex,
3842 collectibleImpact.x,
3843 collectibleImpact.y,
3844 collectibleImpact.z,
3845 destroyedCount));
3846 continue;
3847 }
3848
3849 const ProjectileTraceHit segmentHit = traceProjectileSegment(previous, bullet.position);
3850 if (segmentHit.type != ProjectileHitType::None) {
3851
3852 if (segmentHit.type == ProjectileHitType::Floor) {
3853 createExplosion(glm::vec3(segmentHit.impact.x, 0.0f, segmentHit.impact.z), 1500, true);
3854 bullet.active = false;
3855 logEnv(std::format("bullet {} hit floor at ({:.2f}, {:.2f}, {:.2f})",
3856 bulletIndex,
3857 segmentHit.impact.x,
3858 0.0f,
3859 segmentHit.impact.z));
3860 continue;
3861 }
3862
3863 createExplosion(segmentHit.impact, 1500, true);
3864 bullet.active = false;
3865 logEnv(std::format("bullet {} hit {} at ({:.2f}, {:.2f}, {:.2f})",
3866 bulletIndex,
3867 (segmentHit.type == ProjectileHitType::Pillar) ? "pillar" : "wall",
3868 segmentHit.impact.x,
3869 segmentHit.impact.y,
3870 segmentHit.impact.z));
3871 continue;
3872 }
3873
3874 if (bullet.lifetime >= bullet.maxLifetime) {
3875 bullet.active = false;
3876 logEnv(std::format("bullet {} expired after {:.2f}s", bulletIndex, bullet.lifetime));
3877 continue;
3878 }
3879
3880 if (bullet.distanceTraveled >= bullet.maxDistance) {
3881 bullet.active = false;
3882 logEnv(std::format("bullet {} faded after traveling {:.2f} units", bulletIndex, bullet.distanceTraveled));
3883 }
3884 }
3885
3886 bullets.erase(std::remove_if(bullets.begin(), bullets.end(), [](const Projectile &b) { return !b.active; }), bullets.end());
3887 }
3888
3889 void updateCollectibles(float deltaTime) {
3890 for (Collectible &obj : world.collectibles()) {
3891 if (!obj.active) {
3892 continue;
3893 }
3894 obj.rotation.y += obj.rotationSpeed * deltaTime;
3895 if (obj.rotation.y > 360.0f) {
3896 obj.rotation.y -= 360.0f;
3897 }
3898 }
3899
3900 collectibleClusterResolveTimer += deltaTime;
3901 if (collectibleClusterResolveTimer >= 0.75f) {
3902 collectibleClusterResolveTimer = 0.0f;
3903 resolveCollectibleClusters(2.0f, 2);
3904 }
3905 }
3906
3907 void createExplosion(const glm::vec3 &position, int requestedCount, bool isRed) {
3908 if (requestedCount <= 0) {
3909 return;
3910 }
3911
3912 constexpr float pi = 3.14159265358979323846f;
3913 std::uniform_real_distribution<float> speedDist(3.0f, 15.0f);
3914 std::uniform_real_distribution<float> angleDist(0.0f, 2.0f * pi);
3915 std::uniform_real_distribution<float> elevationDist(-(pi / 6.0f), pi / 3.0f);
3916 std::uniform_real_distribution<float> colorDist(0.7f, 1.0f);
3917
3918 const int count = std::min(requestedCount * 2, 800);
3919 logEnv(std::format("explosion at ({:.2f}, {:.2f}, {:.2f}) particles={} style={}",
3920 position.x,
3921 position.y,
3922 position.z,
3923 count,
3924 isRed ? "impact" : "collectible"));
3925 for (int i = 0; i < count; ++i) {
3926 ExplosionParticle p{};
3927 p.position = position;
3928 const float theta = angleDist(rng);
3929 const float phi = elevationDist(rng);
3930 const float v = speedDist(rng);
3931 p.velocity = glm::vec3(v * std::cos(phi) * std::cos(theta), v * std::sin(phi), v * std::cos(phi) * std::sin(theta));
3932 if (isRed) {
3933 p.color = glm::vec3(colorDist(rng), colorDist(rng) * 0.3f, colorDist(rng) * 0.1f);
3934 } else {
3935 p.color = glm::vec3(colorDist(rng), colorDist(rng) * 0.7f, colorDist(rng) * 0.2f);
3936 }
3937 p.maxLifetime = 0.55f;
3938 p.size = 0.08f + (v * 0.010f);
3939 explosionParticles.push_back(p);
3940 }
3941 }
3942
3943 void updateExplosions(float deltaTime) {
3944 for (ExplosionParticle &particle : explosionParticles) {
3945 if (!particle.active) {
3946 continue;
3947 }
3948 particle.position += particle.velocity * deltaTime;
3949 particle.velocity.y -= 9.8f * deltaTime;
3950
3951 for (const PillarInstance &pillar : world.pillars()) {
3952 const glm::vec2 particle2d(particle.position.x, particle.position.z);
3953 const glm::vec2 pillar2d(pillar.position.x, pillar.position.z);
3954 const float distance = glm::length(particle2d - pillar2d);
3955 if (distance < pillar.radius && particle.position.y > 0.0f && particle.position.y < pillar.height) {
3956 glm::vec2 normal(1.0f, 0.0f);
3957 if (distance > 0.00001f) {
3958 normal = glm::normalize(particle2d - pillar2d);
3959 }
3960 const glm::vec2 vel2d(particle.velocity.x, particle.velocity.z);
3961 const glm::vec2 reflected = vel2d - 2.0f * glm::dot(vel2d, normal) * normal;
3962 particle.velocity.x = reflected.x * 0.5f;
3963 particle.velocity.z = reflected.y * 0.5f;
3964 const glm::vec2 correction = normal * (pillar.radius - distance + 0.1f);
3965 particle.position.x += correction.x;
3966 particle.position.z += correction.y;
3967 }
3968 }
3969
3970 for (const WallSegment &wall : world.walls()) {
3971 glm::vec3 wallDir = wall.end - wall.start;
3972 const float wallLength = glm::length(wallDir);
3973 if (wallLength < 0.0001f) {
3974 continue;
3975 }
3976 wallDir = glm::normalize(wallDir);
3977 const glm::vec3 toStart = particle.position - wall.start;
3978 float projection = glm::dot(toStart, wallDir);
3979 projection = glm::clamp(projection, 0.0f, wallLength);
3980 glm::vec3 closest = wall.start + wallDir * projection;
3981 closest.y = particle.position.y;
3982 const float distance = glm::length(particle.position - closest);
3983 if (distance < 0.5f && particle.position.y >= 0.0f && particle.position.y <= wall.height) {
3984 glm::vec3 normal(1.0f, 0.0f, 0.0f);
3985 if (distance > 0.0001f) {
3986 normal = glm::normalize(particle.position - closest);
3987 }
3988 particle.velocity = glm::reflect(particle.velocity, normal) * 0.5f;
3989 particle.position += normal * 0.2f;
3990 }
3991 }
3992
3993 if (particle.position.y < 0.0f) {
3994 particle.position.y = 0.0f;
3995 particle.velocity.y = -particle.velocity.y * 0.3f;
3996 particle.velocity.x *= 0.8f;
3997 particle.velocity.z *= 0.8f;
3998 }
3999
4000 particle.lifetime += deltaTime;
4001 particle.size *= 0.98f;
4002 if (particle.lifetime >= particle.maxLifetime) {
4003 particle.active = false;
4004 }
4005 }
4006
4007 explosionParticles.erase(
4008 std::remove_if(explosionParticles.begin(), explosionParticles.end(), [](const ExplosionParticle &p) {
4009 return !p.active;
4010 }),
4011 explosionParticles.end());
4012 }
4013
4014 [[nodiscard]] bool lineHitWall(const glm::vec3 &from, const glm::vec3 &to, glm::vec3 &impactOut) const {
4015 const glm::vec3 dir = to - from;
4016 constexpr float bulletRadius = 0.015f;
4017 const float travel = glm::length(dir);
4018 if (travel <= 1e-8f) {
4019 return false;
4020 }
4021
4022 constexpr float sampleStride = 0.05f;
4023 const int steps = std::max(1, static_cast<int>(std::ceil(travel / sampleStride)));
4024 float previousT = 0.0f;
4025 for (int i = 0; i <= steps; ++i) {
4026 const float t = static_cast<float>(i) / static_cast<float>(steps);
4027 const glm::vec3 point = from + (dir * t);
4028 if (pointHitsWall3D(point, bulletRadius)) {
4029 float lo = previousT;
4030 float hi = t;
4031 for (int iter = 0; iter < 10; ++iter) {
4032 const float mid = 0.5f * (lo + hi);
4033 const glm::vec3 midPoint = from + (dir * mid);
4034 if (pointHitsWall3D(midPoint, bulletRadius)) {
4035 hi = mid;
4036 } else {
4037 lo = mid;
4038 }
4039 }
4040 impactOut = from + (dir * hi);
4041 return true;
4042 }
4043 previousT = t;
4044 }
4045 return false;
4046 }
4047
4048 [[nodiscard]] bool lineHitPillar(const glm::vec3 &from, const glm::vec3 &to, glm::vec3 &impactOut) const {
4049 const glm::vec3 dir = to - from;
4050 constexpr float bulletRadius = 0.015f;
4051 const float travel = glm::length(dir);
4052 if (travel <= 1e-8f) {
4053 return false;
4054 }
4055
4056 constexpr float sampleStride = 0.05f;
4057 const int steps = std::max(1, static_cast<int>(std::ceil(travel / sampleStride)));
4058 float previousT = 0.0f;
4059 for (int i = 0; i <= steps; ++i) {
4060 const float t = static_cast<float>(i) / static_cast<float>(steps);
4061 const glm::vec3 point = from + (dir * t);
4062 if (pointHitsPillar3D(point, bulletRadius)) {
4063 float lo = previousT;
4064 float hi = t;
4065 for (int iter = 0; iter < 10; ++iter) {
4066 const float mid = 0.5f * (lo + hi);
4067 const glm::vec3 midPoint = from + (dir * mid);
4068 if (pointHitsPillar3D(midPoint, bulletRadius)) {
4069 hi = mid;
4070 } else {
4071 lo = mid;
4072 }
4073 }
4074 impactOut = from + (dir * hi);
4075 return true;
4076 }
4077 previousT = t;
4078 }
4079 return false;
4080 }
4081
4082 [[nodiscard]] bool lineHitCollectible(const glm::vec3 &from, const glm::vec3 &to, size_t &indexOut, glm::vec3 &impactOut) const {
4083 const glm::vec3 dir = to - from;
4084 const float dirLen2 = glm::dot(dir, dir);
4085 constexpr float bulletRadius = 0.015f;
4086 if (dirLen2 <= 1e-8f) {
4087 return false;
4088 }
4089
4090 bool found = false;
4091 float bestT = 2.0f;
4092 size_t bestIndex = 0;
4093
4094 const std::vector<Collectible> &collectibles = world.collectibles();
4095 for (size_t i = 0; i < collectibles.size(); ++i) {
4096 const Collectible &obj = collectibles[i];
4097 if (!obj.active) {
4098 continue;
4099 }
4100
4101 float tHit = 2.0f;
4102 bool hit = false;
4103
4104 if (obj.type == Collectible::Type::Bird) {
4105 const glm::vec3 halfExtents(obj.radius + bulletRadius);
4106 const glm::vec3 center = obj.position + obj.hitCenterOffset;
4107 const glm::vec3 boxMin = center - halfExtents;
4108 const glm::vec3 boxMax = center + halfExtents;
4109
4110 float tMin = 0.0f;
4111 float tMax = 1.0f;
4112 bool slabMiss = false;
4113
4114 for (int axis = 0; axis < 3; ++axis) {
4115 const float origin = from[axis];
4116 const float delta = dir[axis];
4117 const float minB = boxMin[axis];
4118 const float maxB = boxMax[axis];
4119
4120 if (std::abs(delta) <= 1e-8f) {
4121 if (origin < minB || origin > maxB) {
4122 slabMiss = true;
4123 break;
4124 }
4125 continue;
4126 }
4127
4128 float t0 = (minB - origin) / delta;
4129 float t1 = (maxB - origin) / delta;
4130 if (t0 > t1) {
4131 std::swap(t0, t1);
4132 }
4133
4134 tMin = std::max(tMin, t0);
4135 tMax = std::min(tMax, t1);
4136 if (tMin > tMax) {
4137 slabMiss = true;
4138 break;
4139 }
4140 }
4141
4142 if (!slabMiss) {
4143 hit = true;
4144 tHit = tMin;
4145 }
4146 } else {
4147 const glm::vec3 center = obj.position + obj.hitCenterOffset;
4148 const glm::vec3 m = from - center;
4149 const float a = dirLen2;
4150 const float b = 2.0f * glm::dot(m, dir);
4151 const float hitRadius = obj.radius + bulletRadius;
4152 const float c = glm::dot(m, m) - (hitRadius * hitRadius);
4153 const float discriminant = (b * b) - (4.0f * a * c);
4154 if (discriminant >= 0.0f) {
4155 const float sqrtD = std::sqrt(discriminant);
4156 const float invDen = 1.0f / (2.0f * a);
4157 const float t0 = (-b - sqrtD) * invDen;
4158 const float t1 = (-b + sqrtD) * invDen;
4159 if (t0 >= 0.0f && t0 <= 1.0f) {
4160 hit = true;
4161 tHit = t0;
4162 } else if (t1 >= 0.0f && t1 <= 1.0f) {
4163 hit = true;
4164 tHit = t1;
4165 }
4166 }
4167 }
4168
4169 if (hit && tHit >= 0.0f && tHit <= 1.0f && tHit < bestT) {
4170 found = true;
4171 bestT = tHit;
4172 bestIndex = i;
4173 }
4174 }
4175
4176 if (!found) {
4177 return false;
4178 }
4179
4180 indexOut = bestIndex;
4181 impactOut = from + (dir * bestT);
4182 return true;
4183 }
4184
4185 [[nodiscard]] bool pointHitsWall3D(const glm::vec3 &point, float radius) const {
4186 const float halfThickness = (world.wallThickness() * 0.5f) + radius;
4187 const float halfThicknessSq = halfThickness * halfThickness;
4188 for (const WallSegment &wall : world.walls()) {
4189 if (point.y < 0.0f || point.y > wall.height) {
4190 continue;
4191 }
4192
4193 const glm::vec2 start(wall.start.x, wall.start.z);
4194 const glm::vec2 end(wall.end.x, wall.end.z);
4195 const glm::vec2 seg = end - start;
4196 const float segLen2 = glm::dot(seg, seg);
4197 if (segLen2 <= 1e-8f) {
4198 continue;
4199 }
4200
4201 const glm::vec2 p(point.x, point.z);
4202 const glm::vec2 toPoint = p - start;
4203 const float t = glm::clamp(glm::dot(toPoint, seg) / segLen2, 0.0f, 1.0f);
4204 const glm::vec2 closest = start + (seg * t);
4205 const glm::vec2 d = p - closest;
4206 if (glm::dot(d, d) <= halfThicknessSq) {
4207 return true;
4208 }
4209 }
4210 return false;
4211 }
4212
4213 [[nodiscard]] bool pointHitsPillar3D(const glm::vec3 &point, float radius) const {
4214 for (const PillarInstance &pillar : world.pillars()) {
4215 if (point.y < 0.0f || point.y > pillar.height) {
4216 continue;
4217 }
4218
4219 const glm::vec2 p(point.x, point.z);
4220 const glm::vec2 c(pillar.position.x, pillar.position.z);
4221 const float hitRadius = pillar.radius + radius;
4222 const glm::vec2 d = p - c;
4223 if (glm::dot(d, d) <= (hitRadius * hitRadius)) {
4224 return true;
4225 }
4226 }
4227 return false;
4228 }
4229
4230 [[nodiscard]] float birdGroundYForScale(float scale) const {
4231 const glm::vec3 extent = birdModel.modelAxisExtent();
4232 const glm::vec3 centerOffset = birdModel.modelCenterOffset();
4233 const float modelMinY = -centerOffset.y - (extent.y * 0.5f);
4234 const float clampedScale = std::max(scale, 0.0001f);
4235 return std::max(0.0f, -modelMinY * clampedScale);
4236 }
4237
4238 [[nodiscard]] float birdHitHalfSideForScale(float scale) const {
4239 const glm::vec3 extent = birdModel.modelAxisExtent();
4240 const float modelSide = std::max({extent.x, extent.y, extent.z, 0.0001f});
4241 const float clampedScale = std::max(scale, 0.0001f);
4242 return 0.5f * modelSide * clampedScale;
4243 }
4244
4245 [[nodiscard]] float saturnHitRadiusForScale(float scale) const {
4246 const glm::vec3 extent = saturnModel.modelAxisExtent();
4247 const float modelDiameter = std::max({extent.x, extent.y, extent.z, 0.0001f});
4248 const float clampedScale = std::max(scale, 0.0001f);
4249 return 0.5f * modelDiameter * clampedScale;
4250 }
4251
4252 [[nodiscard]] glm::vec3 saturnHitCenterOffsetForScale(float scale) const {
4253 const glm::vec3 centerOffset = saturnModel.modelCenterOffset();
4254 const float clampedScale = std::max(scale, 0.0001f);
4255 return glm::vec3(-centerOffset.x * clampedScale,
4256 -centerOffset.y * clampedScale,
4257 -centerOffset.z * clampedScale);
4258 }
4259
4260 [[nodiscard]] glm::vec3 birdHitCenterOffsetForScale(float scale) const {
4261 const glm::vec3 centerOffset = birdModel.modelCenterOffset();
4262 const float clampedScale = std::max(scale, 0.0001f);
4263 return glm::vec3(0.0f, -centerOffset.y * clampedScale, 0.0f);
4264 }
4265
4266 [[nodiscard]] float birdSpawnClearanceRadiusForScale(float scale) const {
4267 const glm::vec3 extent = birdModel.modelAxisExtent();
4268 const glm::vec3 centerOffset = birdModel.modelCenterOffset();
4269 const float clampedScale = std::max(scale, 0.0001f);
4270
4271 const float halfXFromOrigin = (extent.x * 0.5f) + std::abs(centerOffset.x);
4272 const float halfZFromOrigin = (extent.z * 0.5f) + std::abs(centerOffset.z);
4273 const float horizontalRadius = std::max(halfXFromOrigin, halfZFromOrigin) * clampedScale;
4274 return horizontalRadius + 0.05f;
4275 }
4276
4277 [[nodiscard]] float placementRadiusForCollectible(const Collectible &obj) const {
4278 if (obj.type == Collectible::Type::Bird) {
4279 return std::max(obj.radius, birdSpawnClearanceRadiusForScale(obj.scale.x));
4280 }
4281 return obj.radius;
4282 }
4283
4284 void normalizeCollectiblesToModel() {
4285 for (Collectible &obj : world.collectibles()) {
4286 if (obj.type == Collectible::Type::Bird) {
4287 obj.radius = birdHitHalfSideForScale(obj.scale.x);
4288 obj.hitCenterOffset = birdHitCenterOffsetForScale(obj.scale.x);
4289 obj.position.y = birdGroundYForScale(obj.scale.x);
4290 } else {
4291 obj.radius = saturnHitRadiusForScale(obj.scale.x);
4292 obj.hitCenterOffset = saturnHitCenterOffsetForScale(obj.scale.x);
4293 }
4294 }
4295
4296 resolveCollectibleEnvironmentCollisions();
4297 resolveCollectibleOverlaps();
4298 resolveCollectibleClusters(2.0f, 5);
4299 }
4300
4301 [[nodiscard]] bool overlapsCollectibleAt(const glm::vec3 &candidate,
4302 float radius,
4303 size_t ignoreIndex,
4304 bool includeInactive) const {
4305 const std::vector<Collectible> &collectibles = world.collectibles();
4306 for (size_t i = 0; i < collectibles.size(); ++i) {
4307 if (i == ignoreIndex) {
4308 continue;
4309 }
4310 const Collectible &other = collectibles[i];
4311 if (!includeInactive && !other.active) {
4312 continue;
4313 }
4314
4315 const float separation = std::max(5.0f, other.radius + radius + 0.2f);
4316 if (glm::length(other.position - candidate) < separation) {
4317 return true;
4318 }
4319 }
4320 return false;
4321 }
4322
4323 bool relocateCollectible(size_t index, float minMoveDistance, int maxAttempts) {
4324 std::vector<Collectible> &collectibles = world.collectibles();
4325 if (index >= collectibles.size()) {
4326 return false;
4327 }
4328
4329 Collectible &obj = collectibles[index];
4330 const glm::vec3 oldPosition = obj.position;
4331 const float y = (obj.type == Collectible::Type::Bird) ? birdGroundYForScale(obj.scale.x) : 2.5f;
4332 const float placementRadius = placementRadiusForCollectible(obj);
4333
4334 for (int attempt = 0; attempt < maxAttempts; ++attempt) {
4335 glm::vec3 candidate{};
4336 if (!sampleNavigablePoint(y, placementRadius, candidate, 4)) {
4337 continue;
4338 }
4339 if (glm::length(candidate - oldPosition) < minMoveDistance) {
4340 continue;
4341 }
4342 if (overlapsCollectibleAt(candidate, obj.radius, index, false)) {
4343 continue;
4344 }
4345
4346 obj.position = candidate;
4347 return true;
4348 }
4349
4350 return false;
4351 }
4352
4353 void resolveCollectibleEnvironmentCollisions() {
4354 std::vector<Collectible> &collectibles = world.collectibles();
4355 for (size_t i = 0; i < collectibles.size(); ++i) {
4356 if (!collectibles[i].active) {
4357 continue;
4358 }
4359
4360 const float placementRadius = placementRadiusForCollectible(collectibles[i]);
4361 if (!world.checkWallCollision(collectibles[i].position, placementRadius) &&
4362 !world.checkPillarCollision(collectibles[i].position, placementRadius)) {
4363 continue;
4364 }
4365
4366 relocateCollectible(i, 2.0f, 512);
4367 }
4368 }
4369
4370 void resolveCollectibleOverlaps() {
4371 std::vector<Collectible> &collectibles = world.collectibles();
4372 for (size_t i = 0; i < collectibles.size(); ++i) {
4373 if (!collectibles[i].active) {
4374 continue;
4375 }
4376 if (!overlapsCollectibleAt(collectibles[i].position, collectibles[i].radius, i, false)) {
4377 continue;
4378 }
4379 relocateCollectible(i, 1.5f, 320);
4380 }
4381 }
4382
4383 void resolveCollectibleClusters(float minVisualSeparation, int passes) {
4384 if (minVisualSeparation <= 0.0f || passes <= 0) {
4385 return;
4386 }
4387
4388 std::vector<Collectible> &collectibles = world.collectibles();
4389 const float minVisualSeparationSq = minVisualSeparation * minVisualSeparation;
4390 for (int pass = 0; pass < passes; ++pass) {
4391 bool movedAny = false;
4392 for (size_t i = 0; i < collectibles.size(); ++i) {
4393 if (!collectibles[i].active) {
4394 continue;
4395 }
4396
4397 for (size_t j = i + 1; j < collectibles.size(); ++j) {
4398 if (!collectibles[j].active) {
4399 continue;
4400 }
4401
4402 const glm::vec3 delta = collectibles[j].position - collectibles[i].position;
4403 if (glm::dot(delta, delta) >= minVisualSeparationSq) {
4404 continue;
4405 }
4406
4407 if (relocateCollectible(j, minVisualSeparation, 512)) {
4408 movedAny = true;
4409 }
4410 }
4411 }
4412
4413 if (!movedAny) {
4414 break;
4415 }
4416 }
4417 }
4418
4419 void disperseNearbyCollectibles(const glm::vec3 &center, float radius, size_t ignoreIndex) {
4420 std::vector<Collectible> &collectibles = world.collectibles();
4421 for (size_t i = 0; i < collectibles.size(); ++i) {
4422 if (i == ignoreIndex) {
4423 continue;
4424 }
4425 if (!collectibles[i].active) {
4426 continue;
4427 }
4428 if (glm::length(collectibles[i].position - center) > radius) {
4429 continue;
4430 }
4431
4432 relocateCollectible(i, std::max(3.0f, radius), 256);
4433 }
4434 }
4435
4436 [[nodiscard]] bool deactivateCollectibleAt(size_t index) {
4437 std::vector<Collectible> &collectibles = world.collectibles();
4438 if (index >= collectibles.size()) {
4439 return false;
4440 }
4441
4442 Collectible &obj = collectibles[index];
4443 if (!obj.active) {
4444 return false;
4445 }
4446
4447 obj.active = false;
4448 return true;
4449 }
4450
4451 std::string assetRoot;
4452 std::string shaderRoot;
4453 std::string modelRoot;
4454
4455 MazeWorld world{};
4456 mxvk::VKAbstractModel floorModel{};
4457 RawWallRenderer rawWallRenderer{};
4458 RawPillarRenderer rawPillarRenderer{};
4459 mxvk::VKAbstractModel saturnModel{};
4460 mxvk::VKAbstractModel birdModel{};
4461 mxvk::VKAbstractModel blasterModel{};
4462 mxvk::VKAbstractModel bulletModel{};
4463
4464 VkPipelineLayout pointPipelineLayout = VK_NULL_HANDLE;
4465 VkPipeline pointPipeline = VK_NULL_HANDLE;
4466 VkBuffer pointVertexBuffer = VK_NULL_HANDLE;
4467 VkDeviceMemory pointVertexMemory = VK_NULL_HANDLE;
4468 void *pointVertexMapped = nullptr;
4469 size_t maxPointVertices = 200000;
4470 std::string pointParticleVertSpv{};
4471 std::string pointParticleFragSpv{};
4472 std::string modelVertSpv{};
4473 std::string pillarVertSpv{};
4474 std::string wallFragSpv{};
4475 std::string floorFragSpv{};
4476 std::string pillarFragSpv{};
4477 std::string objectFragSpv{};
4478 std::string bulletFragSpv{};
4479
4480 std::vector<Projectile> bullets{};
4481 std::vector<ExplosionParticle> explosionParticles{};
4482 std::mt19937 rng{std::random_device{}()};
4483
4484 glm::vec3 cameraPos{0.0f, 1.7f, 0.0f};
4485 glm::vec3 cameraFront{0.0f, 0.0f, -1.0f};
4486 float yaw = -90.0f;
4487 float pitch = 0.0f;
4488 bool mouseCapture = true;
4489 bool firstMouse = true;
4490 bool suppressProjectileOnNextLeftDown = false;
4491 bool showFps = true;
4492 float mouseSensitivity = 0.15f;
4493
4494 float jumpVelocity = 0.0f;
4495 float gravity = 0.015f;
4496 float collectibleClusterResolveTimer = 0.0f;
4497 uint32_t destroyedCount = 0;
4498
4499 SDL_Gamepad *gamepad = nullptr;
4500 SDL_JoystickID gamepadId = 0;
4501 int stickDeadZone = 8000;
4502 float controllerLookSensitivity = 2.0f;
4503
4504 std::chrono::steady_clock::time_point lastTick{std::chrono::steady_clock::now()};
4505 };
4506
4507} // namespace walk
4508
4509int main(int argc, char **argv) {
4510 try {
4511 const Arguments args = proc_args(argc, argv);
4512 walk::WalkWindow window(args);
4513 window.loop();
4514 } catch (mxvk::Exception &e) {
4515 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
4516 return EXIT_FAILURE;
4517 } catch (ArgException<std::string> &e) {
4518 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
4519 return EXIT_FAILURE;
4520 }
4521
4522 return EXIT_SUCCESS;
4523}
Lightweight, header-only, template command-line argument parser.
Arguments proc_args(int &argc, char **argv)
Parse standard libmx2 command-line options from main()'s argv.
Definition argz.hpp:872
Exception thrown by Argz::proc() on unrecognised or malformed options.
Definition argz.hpp:178
std::string text() const
void setBackfaceCulling(bool enabled)
Enable or disable backface culling for this model pipeline.
void updateUBO(uint32_t imageIndex, const UniformBufferObject &ubo)
Update one per-frame UBO payload.
float modelRenderScale() const
Access the computed render scale used for normalization.
void load(VK_Window *window, const std::string &modelPath, const std::string &textureManifestPath, const std::string &textureBasePath, float scale=1.0f)
Load mesh/texture resources and build Vulkan state.
void setShaders(VK_Window *window, const std::string &vertSpv, const std::string &fragSpv)
Configure custom shader paths and rebuild pipelines.
glm::vec3 modelCenterOffset() const
Access the computed center offset used for normalization.
void render(VkCommandBuffer cmd, uint32_t imageIndex, bool wireframe=false) const
Record draw commands for this model.
void event(SDL_Event &e) override
Handle one SDL event.
VK_IOWindow(const std::string &path, const std::string &title, const int width, const int height, const bool fullscreen, const bool enableVsync=false)
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:37
VkExtent2D getSwapchainExtent() const noexcept
Get the current swapchain extent.
Definition mxvk.hpp:186
void loop()
Run the main event/render loop.
Definition mxvk.cpp:600
VkDevice getDevice() const noexcept
Get the Vulkan logical device handle.
Definition mxvk.hpp:168
VkDevice device
Definition mxvk.hpp:485
SDL_Window * getSDLWindow() const noexcept
Get the underlying SDL window handle.
Definition mxvk.hpp:165
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
Definition mxvk.cpp:593
void exit()
Request loop termination.
Definition mxvk.cpp:1126
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
Definition mxvk.cpp:2991
void printText(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for rendering during the current frame.
Definition mxvk.cpp:3018
static std::vector< char > loadSpv(const std::string &path)
Load a SPIR-V file from disk.
Definition mxvk.cpp:141
const std::vector< PillarInstance > & pillars() const noexcept
Definition room.cpp:100
bool checkCollectibleCollision(const glm::vec3 &point, size_t &indexOut) const
Definition room.cpp:167
std::vector< Collectible > & collectibles() noexcept
Definition room.cpp:102
glm::vec3 startPosition() const noexcept
Definition room.cpp:96
bool checkWallCollision(const glm::vec3 &position, float radius) const
Definition room.cpp:134
float wallThickness() const noexcept
Definition room.cpp:217
bool checkPillarCollision(const glm::vec3 &position, float playerRadius) const
Definition room.cpp:156
const std::vector< WallSegment > & walls() const noexcept
Definition room.cpp:98
const std::vector< Collectible > & collectibles() const noexcept
Definition room.cpp:104
int activeCollectibles() const
Definition room.cpp:106
void generate(uint32_t seed)
Definition room.cpp:116
glm::vec3 randomPointInCell(int cellX, int cellZ, float objectRadius, float y, std::mt19937 &rng, float margin) const
Definition room.cpp:194
void load(mxvk::VK_Window *targetWindow, const std::string &textureManifestPath, const std::string &textureBasePath, const std::vector< char > &vertSpv, const std::vector< char > &fragSpv)
Definition room.cpp:552
void resize(mxvk::VK_Window *targetWindow)
Definition room.cpp:578
void reloadFragShader(const std::vector< char > &newFragSpv)
Hot-swap the fragment shader without rebuilding geometry or descriptors.
Definition room.cpp:595
void render(VkCommandBuffer cmd, uint32_t imageIndex, const std::vector< PillarInstance > &pillars, const glm::mat4 &view, const glm::mat4 &proj, const glm::vec4 &fx)
Definition room.cpp:618
void cleanup(mxvk::VK_Window *targetWindow)
Definition room.cpp:605
void reloadFragShader(const std::vector< char > &newFragSpv)
Hot-swap the fragment shader without rebuilding geometry or descriptors.
Definition room.cpp:1576
void resize(mxvk::VK_Window *targetWindow)
Definition room.cpp:1559
void load(mxvk::VK_Window *targetWindow, const std::string &textureManifestPath, const std::string &textureBasePath, const std::vector< char > &vertexShaderSpv, const std::vector< char > &fragmentShaderSpv)
Definition room.cpp:1533
void render(VkCommandBuffer cmd, uint32_t imageIndex, const std::vector< WallSegment > &walls, float wallThickness, const glm::mat4 &view, const glm::mat4 &proj, const glm::vec4 &fx)
Definition room.cpp:1599
void cleanup(mxvk::VK_Window *targetWindow)
Definition room.cpp:1586
~WalkWindow() override
Definition room.cpp:2492
void event(SDL_Event &e) override
Handle one SDL event.
Definition room.cpp:2506
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override
Optional hook for derived classes to record extra draw commands.
Definition room.cpp:2644
void console_proc() override
Definition room.cpp:2601
WalkWindow(const Arguments &args)
Definition room.cpp:2413
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
Definition room.cpp:2632
void console_event(SDL_Event &e) override
Definition room.cpp:2530
int main(void)
Definition main.cpp:7
High-level model wrapper integrated with MXVK dynamic rendering.
PNG image loading and saving utilities via SDL3.
Utilities for loading and saving PNG images.
Definition mxvk.hpp:30
VkShaderModule create_shader_module(VkDevice device, const std::vector< char > &spv_bytes)
Create a shader module from SPIR-V bytecode.
SDL_Surface * LoadPNG(const char *file)
Load a PNG file into an SDL_Surface.
Definition mxvk_png.cpp:103
std::default_random_engine & rng()
Returns the thread-local random number engine used by simulation helpers.
Definition room.cpp:29
std::atomic< bool > active
Definition relay.cpp:12
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:730
bool fullscreen
Whether fullscreen mode was requested.
Definition argz.hpp:736
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:733
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:732
float y
Definition space.cpp:59
float x
Definition space.cpp:59
bool active
Definition space.cpp:62
float lifetime
Definition space.cpp:61
float rotationSpeed
Definition room.cpp:54
glm::vec3 hitCenterOffset
Definition room.cpp:51
glm::vec3 scale
Definition room.cpp:53
glm::vec3 rotation
Definition room.cpp:52
glm::vec3 position
Definition room.cpp:50
glm::vec3 position
Definition room.cpp:38
glm::vec3 direction
Definition room.cpp:67
float distanceTraveled
Definition room.cpp:71
glm::vec3 position
Definition room.cpp:66
std::vector< TrailPoint > trail
Definition room.cpp:74
float maxDistance
Definition room.cpp:72
float lifetime
Definition room.cpp:69
float trailTimer
Definition room.cpp:75
float maxLifetime
Definition room.cpp:70
glm::vec3 start
Definition room.cpp:32
glm::vec3 end
Definition room.cpp:33