MXVK Vulkan Framework 0.35.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1#include "mxvk/argz.hpp"
2#include "mxvk/mxvk.hpp"
4#if defined(MXVK_USE_EIGEN_MATH)
6#else
7#include "mxvk/mxvk_math.h"
8#endif
9
10#include <SDL3/SDL.h>
11
12#include <algorithm>
13#include <array>
14#include <cmath>
15#include <cstdint>
16#include <cstdlib>
17#include <cstring>
18#include <filesystem>
19#include <format>
20#include <fstream>
21#include <iostream>
22#include <memory>
23#include <random>
24#include <string>
25#include <utility>
26#include <vector>
27
28namespace {
29
30 constexpr int board_rows = 18;
31 constexpr int board_cols = 8;
32 constexpr int piece_height = 3;
33 constexpr int max_scores = 8;
34 constexpr int base_width = 1440;
35 constexpr int base_height = 1080;
36 constexpr int game_base_width = 640;
37 constexpr int game_base_height = 480;
38 constexpr int game_board_start_x = 184;
39 constexpr int game_board_start_y = 78;
40 constexpr int game_block_width = 31;
41 constexpr int game_block_height = 14;
42 constexpr int game_block_spacing = 1;
43 constexpr int GRID_VERTICAL_GAP = 5;
44 constexpr int game_next_panel_x = 450;
45 constexpr int game_next_panel_y = 180;
46 constexpr int menu_item_count = 4;
47 constexpr int title_screen_time_ms = 1500;
48 constexpr int flash_time_ms = 180;
49 constexpr int lines_per_speedup = 10;
50 constexpr int score_points_per_match = 6;
51 constexpr int max_name_length = 16;
52 constexpr Uint32 joy_repeat_delay_ms = 180;
53 constexpr Sint16 joystick_dead_zone = 16000;
54 constexpr mxvk::MXCOLOR transparent_black = 0x00000000U;
55 constexpr float GRID_YAW_SPEED = 115.0f;
56 constexpr float GRID_PITCH_SPEED = 90.0f;
57 constexpr float GRID_ZOOM_SPEED = 3.2f;
58 constexpr float GRID_MIN_PITCH = -70.0f;
59 constexpr float GRID_MAX_PITCH = 70.0f;
60 constexpr float GRID_MIN_CAMERA_DISTANCE = 1.65f;
61 constexpr float GRID_MAX_CAMERA_DISTANCE = 9.0f;
62 constexpr float GRID_DEFAULT_CAMERA_DISTANCE = 2.45f;
63
72
73 struct Cell {
74 int color = 0;
75 Uint64 flash_until = 0;
76 };
77
78 struct Piece {
79 int x = 3;
80 int y = 0;
81 std::array<int, piece_height> colors{};
82 std::array<int, piece_height> next_colors{};
83 };
84
85 struct ScoreEntry {
86 std::string name;
87 int score = 0;
88 };
89
90 class SurfaceDeleter {
91 public:
92 void operator()(SDL_Surface *surface) const { SDL_DestroySurface(surface); }
93 };
94
95 using SurfacePtr = std::unique_ptr<SDL_Surface, SurfaceDeleter>;
96
97 struct FaceDraw {
98 std::array<int, 4> indices{};
99 float depth = 0.0f;
100 mxvk::MXCOLOR color = mxvk::MXVK_RGB(255, 255, 255);
101 };
102
104 int center_x = 0;
105 int center_y = 0;
106 int size = 24;
107 int color = 1;
108 float phase = 0.0f;
109 bool flashing = false;
110 };
111
113 int center_x = 0;
114 int center_y = 0;
115 int size = 24;
116 };
117
118 SurfacePtr createFrameSurface(int width, int height) {
119 SurfacePtr surface(SDL_CreateSurface(width, height, SDL_PIXELFORMAT_RGBA32));
120 if (!surface) {
121 throw mxvk::Exception(std::format("Failed to create 3dmath_masterpiece frame surface: {}", SDL_GetError()));
122 }
123 return surface;
124 }
125
126 bool hasResolutionArgument(int argc, char **argv) {
127 for (int i = 1; i < argc; ++i) {
128 const std::string arg = argv[i] == nullptr ? std::string{} : std::string(argv[i]);
129 if (arg == "-r" || arg == "-R" || arg == "--resolution") {
130 return true;
131 }
132 }
133 return false;
134 }
135
136 std::filesystem::path resolveAssetRoot(const std::string &path) {
137 if (!path.empty() && path != "." && path != "./") {
138 return std::filesystem::path(path);
139 }
140 return std::filesystem::path(MASTERPIECE_ASSET_DIR);
141 }
142
143 std::filesystem::path resolvePuzzleAssetRoot(const std::filesystem::path &asset_root) {
144 const std::filesystem::path shared_root = asset_root.parent_path() / "puzzle";
145 if (std::filesystem::exists(shared_root / "data" / "gamebg.png")) {
146 return shared_root;
147 }
148 return asset_root;
149 }
150
151 std::filesystem::path scorePath(const std::filesystem::path &asset_root) { return asset_root / "data" / "scores.dat"; }
152
154 public:
155 explicit HighScores(std::filesystem::path file_path) : file_path(std::move(file_path)) { load(); }
156
157 void add(std::string name, int score) {
158 normalize(name);
159 entries.push_back({std::move(name), score});
160 sortAndTrim();
161 save();
162 }
163
164 [[nodiscard]] bool qualifies(int score) const {
165 if (entries.size() < max_scores) {
166 return true;
167 }
168 return score > entries.back().score;
169 }
170
171 [[nodiscard]] const std::vector<ScoreEntry> &list() const { return entries; }
172
173 private:
174 std::filesystem::path file_path;
175 std::vector<ScoreEntry> entries;
176
177 static void normalize(std::string &name) {
178 std::string cleaned;
179 cleaned.reserve(name.size());
180 for (unsigned char ch : name) {
181 if (ch >= 32 && ch < 127 && ch != ':') {
182 cleaned.push_back(static_cast<char>(ch));
183 }
184 }
185
186 if (cleaned.empty()) {
187 cleaned = "Player";
188 }
189 if (cleaned.size() > max_name_length) {
190 cleaned.resize(max_name_length);
191 }
192 name = std::move(cleaned);
193 }
194
195 void sortAndTrim() {
196 std::sort(entries.begin(), entries.end(), [](const ScoreEntry &a, const ScoreEntry &b) {
197 if (a.score != b.score) {
198 return a.score > b.score;
199 }
200 return a.name < b.name;
201 });
202
203 if (entries.size() > max_scores) {
204 entries.resize(max_scores);
205 }
206 }
207
208 void initDefaults() {
209 entries.clear();
210 for (int i = 0; i < max_scores; ++i) {
211 entries.push_back({"Anonymous", 0});
212 }
213 }
214
215 void load() {
216 entries.clear();
217
218 std::ifstream in(file_path);
219 if (!in.is_open()) {
220 initDefaults();
221 return;
222 }
223
224 std::string line;
225 while (std::getline(in, line)) {
226 const std::size_t sep = line.find(':');
227 if (sep == std::string::npos) {
228 continue;
229 }
230
231 ScoreEntry entry{};
232 entry.name = line.substr(0, sep);
233 entry.score = static_cast<int>(std::strtol(line.substr(sep + 1).c_str(), nullptr, 10));
234 normalize(entry.name);
235 entries.push_back(std::move(entry));
236 }
237
238 if (entries.empty()) {
239 initDefaults();
240 save();
241 return;
242 }
243
244 sortAndTrim();
245 }
246
247 void save() const {
248 std::error_code ec;
249 std::filesystem::create_directories(file_path.parent_path(), ec);
250
251 std::ofstream out(file_path, std::ios::trunc);
252 if (!out.is_open()) {
253 return;
254 }
255
256 for (const ScoreEntry &entry : entries) {
257 out << entry.name << ':' << entry.score << '\n';
258 }
259 }
260 };
261
262 struct Layout {
263 float scale_x = 1.0f;
264 float scale_y = 1.0f;
265 float game_scale = 1.0f;
268 int game_x = 0;
269 int game_y = 0;
272 int board_x = 185;
273 int board_y = 95;
274 int cell_w = 32;
275 int cell_h = 16;
276 int next_x = 510;
277 int next_y = 200;
278 int menu_x = 505;
279 int menu_y = 400;
280 int menu_w = 430;
281 int menu_h = 82;
282 int menu_step = 118;
283 };
284
285} // namespace
286
287namespace example {
288
289 class MasterPieceWindow final : public mxvk::VK_Window {
290 public:
291 MasterPieceWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync, const FramebufferDimensions &framebuffer) : mxvk::VK_Window("3D Math MasterPiece", width, height, fullscreen, MXVK_VALIDATION, enable_vsync), asset_root(resolveAssetRoot(path)), puzzle_asset_root(resolvePuzzleAssetRoot(resolveAssetRoot(path))), high_scores(scorePath(asset_root)), frame_width(framebuffer.width), frame_height(framebuffer.height) {
292 setClearColor(0.0f, 0.0f, 0.0f, 1.0f);
294
295 const std::string font_path = dataPath("font.ttf");
296 title_font.reset(font_path, 34);
297 ui_font.reset(font_path, 20);
298 setFont(font_path, 20);
299
300 loadSprites();
301 resetGame();
302 setScreen(Screen::Intro);
303 tryOpenFirstGamepad();
304 }
305
306 ~MasterPieceWindow() override { closeGamepad(); }
307
308 void event(SDL_Event &e) override {
309 if (e.type == SDL_EVENT_QUIT) {
310 exit();
311 return;
312 }
313
314 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
315 openGamepad(e.gdevice.which);
316 return;
317 }
318
319 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
320 if (gamepad != nullptr && e.gdevice.which == gamepadId) {
321 closeGamepad();
322 tryOpenFirstGamepad();
323 }
324 return;
325 }
326
327 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
328 handleControllerButton(e.gbutton.button);
329 return;
330 }
331
332 if (screen == Screen::NameEntry && e.type == SDL_EVENT_TEXT_INPUT) {
333 handleNameText(e.text.text);
334 return;
335 }
336
337 if (e.type != SDL_EVENT_KEY_DOWN) {
338 return;
339 }
340
341 switch (screen) {
342 case Screen::Intro:
343 if (isConfirmKey(e.key.key) || e.key.key == SDLK_ESCAPE) {
344 setScreen(Screen::Menu);
345 }
346 break;
347 case Screen::Menu:
348 handleMenuKey(e.key.key);
349 break;
350 case Screen::Game:
351 handleGameKey(e.key.key);
352 break;
353 case Screen::Scores:
354 case Screen::Credits:
355 if (e.key.key == SDLK_RETURN || e.key.key == SDLK_ESCAPE) {
356 setScreen(Screen::Menu);
357 }
358 break;
360 handleNameKey(e.key.key);
361 break;
362 }
363 }
364
365 void proc() override {
366 const Uint64 now = SDL_GetTicks();
367 layout = computeLayout();
368 updateGridViewInput(now);
369 pollController(now);
370
371 switch (screen) {
372 case Screen::Intro:
373 if (!drawIntro(now)) {
374 break;
375 }
376 drawMenu();
377 break;
378 case Screen::Menu:
379 drawMenu();
380 break;
381 case Screen::Game:
382 updateGame(now);
383 if (screen == Screen::Game) {
384 drawGame(now);
385 } else if (screen == Screen::Scores) {
386 drawScores(false);
387 } else if (screen == Screen::Credits) {
388 drawCredits();
389 } else if (screen == Screen::NameEntry) {
390 drawScores(true);
391 } else if (screen == Screen::Menu) {
392 drawMenu();
393 }
394 break;
395 case Screen::Scores:
396 drawScores(false);
397 break;
398 case Screen::Credits:
399 drawCredits();
400 break;
402 drawScores(true);
403 break;
404 }
405 }
406
407 private:
408 static constexpr std::array<const char *, menu_item_count> menu_files{{
409 "menu_new_game.png",
410 "menu_high_scores.png",
411 "menu_credits.png",
412 "menu_quit.png",
413 }};
414
415 std::filesystem::path asset_root;
416 std::filesystem::path puzzle_asset_root;
417 HighScores high_scores;
418 Screen screen = Screen::Intro;
419 Layout layout{};
420 std::mt19937 rng{std::random_device{}()};
421 std::array<std::array<Cell, board_cols>, board_rows> board{};
422 Piece piece{};
423 mxvk::Font title_font{};
424 mxvk::Font ui_font{};
425 mxvk::VK_Sprite *background_intro = nullptr;
426 mxvk::VK_Sprite *mxvk_logo = nullptr;
427 mxvk::VK_Sprite *background_menu = nullptr;
428 mxvk::VK_Sprite *background_game = nullptr;
429 mxvk::VK_Sprite *cursor = nullptr;
430 std::array<mxvk::VK_Sprite *, menu_files.size()> menu_items{};
431 mxvk::VK_Sprite *panel = nullptr;
432 mxvk::VK_Sprite *overlay = nullptr;
433 SurfacePtr frame_surface;
434 const SDL_PixelFormatDetails *frame_format = nullptr;
435 mxvk::VK_Sprite *frame_sprite = nullptr;
436 int frame_width = 1280;
437 int frame_height = 720;
438 std::string player_name;
439 int menu_selection = 0;
440 int score = 0;
441 int lines = 0;
442 int speed_level = 0;
443 int lines_toward_speedup = 0;
444 int fall_delay_ms = 520;
445 Uint64 intro_start_ms = 0;
446 Uint64 last_update_ms = 0;
447 Uint64 fall_accumulator_ms = 0;
448 Uint64 last_view_update_ms = 0;
449 float grid_pitch = 0.0f;
450 float grid_yaw = 0.0f;
451 float camera_distance = GRID_DEFAULT_CAMERA_DISTANCE;
452 Uint32 joy_repeat_left_ms = 0;
453 Uint32 joy_repeat_right_ms = 0;
454 Uint32 joy_repeat_up_ms = 0;
455 Uint32 joy_repeat_down_ms = 0;
456 SDL_Gamepad *gamepad = nullptr;
457 SDL_JoystickID gamepadId = 0;
458 bool paused = false;
459 bool awaiting_name = false;
460 bool score_added = false;
461 bool waiting_for_spawn = false;
462
463 static bool isConfirmKey(SDL_Keycode key) { return key == SDLK_RETURN || key == SDLK_SPACE; }
464
465 static int scaled(int value, float scale) { return std::max(1, static_cast<int>(std::lround(static_cast<float>(value) * scale))); }
466
467 static int scaledPos(int value, float scale) { return static_cast<int>(std::lround(static_cast<float>(value) * scale)); }
468
469 static int flashingSpriteIndex(int x, int y, Uint64 now) {
470 const Uint64 tick = now / 18U;
471 const Uint64 mixed = tick + static_cast<Uint64>(x * 37 + y * 101);
472 return static_cast<int>((mixed % 9U) + 1U);
473 }
474
475 [[nodiscard]] Layout computeLayout() const {
476 Layout result{};
477 const VkExtent2D extent = getSwapchainExtent();
478 result.width = extent.width == 0U ? base_width : static_cast<int>(extent.width);
479 result.height = extent.height == 0U ? base_height : static_cast<int>(extent.height);
480 result.scale_x = static_cast<float>(result.width) / static_cast<float>(base_width);
481 result.scale_y = static_cast<float>(result.height) / static_cast<float>(base_height);
482 result.game_scale = std::min(result.scale_x, result.scale_y);
483 result.game_w = scaled(base_width, result.game_scale);
484 result.game_h = scaled(base_height, result.game_scale);
485 result.game_x = (result.width - result.game_w) / 2;
486 result.game_y = (result.height - result.game_h) / 2;
487 result.board_x = result.game_x + scaledPos(185, result.game_scale);
488 result.board_y = result.game_y + scaledPos(95, result.game_scale);
489 result.cell_w = scaled(32, result.game_scale);
490 result.cell_h = scaled(16, result.game_scale);
491 result.next_x = result.game_x + scaledPos(510, result.game_scale);
492 result.next_y = result.game_y + scaledPos(200, result.game_scale);
493 result.menu_x = scaled(505, result.scale_x);
494 result.menu_y = scaled(400, result.scale_y);
495 result.menu_w = scaled(430, result.scale_x);
496 result.menu_h = scaled(82, result.scale_y);
497 result.menu_step = scaled(118, result.scale_y);
498 return result;
499 }
500
501 std::string dataPath(const char *name) const { return (asset_root / "data" / name).string(); }
502
503 std::string puzzleDataPath(const char *name) const {
504 const std::filesystem::path shared_path = puzzle_asset_root / "data" / name;
505 if (std::filesystem::exists(shared_path)) {
506 return shared_path.string();
507 }
508 return dataPath(name);
509 }
510
511 mxvk::VK_Sprite *loadPngSprite(const char *name) { return createSprite(dataPath(name)); }
512
513 mxvk::VK_Sprite *loadEffectSprite(const char *name) { return createSprite(dataPath(name), "", dataPath("intro.frag.spv")); }
514
515 mxvk::VK_Sprite *makeSolidPixel(std::uint8_t r, std::uint8_t g, std::uint8_t b, std::uint8_t a) {
516 const std::array<std::uint8_t, 4> pixel{r, g, b, a};
517 mxvk::VK_Sprite *sprite = createSprite(1, 1);
518 sprite->updateTexture(pixel.data(), 1, 1, 4);
519 return sprite;
520 }
521
522 void loadSprites() {
523 background_intro = loadEffectSprite("intro.png");
524 background_menu = loadEffectSprite("start.png");
525 background_game = createSprite(dataPath("gamebg.png"));
526 mxvk_logo = createSprite(puzzleDataPath("mxvk_logo.png"));
527 cursor = loadPngSprite("cursor.png");
528
529 for (std::size_t i = 0; i < menu_files.size(); ++i) {
530 menu_items[i] = loadPngSprite(menu_files[i]);
531 }
532
533 panel = makeSolidPixel(0, 0, 0, 192);
534 overlay = makeSolidPixel(0, 0, 0, 128);
535 }
536
537 void setScreen(Screen next) {
538 if (screen == Screen::NameEntry && next != Screen::NameEntry) {
539 SDL_StopTextInput(window.get());
540 }
541
542 screen = next;
543 awaiting_name = (screen == Screen::NameEntry);
544 if (screen == Screen::NameEntry) {
545 player_name.clear();
546 if (!score_added) {
547 SDL_StartTextInput(window.get());
548 }
549 }
550
551 if (screen == Screen::Intro) {
552 intro_start_ms = SDL_GetTicks();
553 }
554 }
555
556 void resetGame() {
557 for (auto &row : board) {
558 for (Cell &cell : row) {
559 cell = {};
560 }
561 }
562
563 score = 0;
564 lines = 0;
565 speed_level = 0;
566 lines_toward_speedup = 0;
567 fall_delay_ms = 520;
568 last_update_ms = 0;
569 fall_accumulator_ms = 0;
570 paused = false;
571 awaiting_name = false;
572 score_added = false;
573 waiting_for_spawn = false;
574 piece.x = board_cols / 2 - 1;
575 piece.y = 0;
576 piece.next_colors = randomColors();
577 spawnPiece();
578 }
579
580 std::array<int, piece_height> randomColors() {
581 std::uniform_int_distribution<int> dist(1, 9);
582 std::array<int, piece_height> colors{dist(rng), dist(rng), dist(rng)};
583 while (colors[0] == colors[1] && colors[1] == colors[2]) {
584 colors[1] = dist(rng);
585 }
586 return colors;
587 }
588
589 bool canPlacePiece(int x, int y) const {
590 if (x < 0 || x >= board_cols) {
591 return false;
592 }
593
594 for (int i = 0; i < piece_height; ++i) {
595 const int row = y + i;
596 if (row < 0 || row >= board_rows) {
597 return false;
598 }
599 if (board[static_cast<std::size_t>(row)][static_cast<std::size_t>(x)].color != 0) {
600 return false;
601 }
602 }
603 return true;
604 }
605
606 bool movePiece(int dx, int dy) {
607 const int next_x = piece.x + dx;
608 const int next_y = piece.y + dy;
609 if (!canPlacePiece(next_x, next_y)) {
610 return false;
611 }
612
613 piece.x = next_x;
614 piece.y = next_y;
615 return true;
616 }
617
618 void dropPiece() {
619 while (movePiece(0, 1)) {
620 }
621 lockPiece(SDL_GetTicks());
622 }
623
624 void rotatePieceColors(bool forward) {
625 if (forward) {
626 const int temp = piece.colors.back();
627 piece.colors[2] = piece.colors[1];
628 piece.colors[1] = piece.colors[0];
629 piece.colors[0] = temp;
630 } else {
631 const int temp = piece.colors.front();
632 piece.colors[0] = piece.colors[1];
633 piece.colors[1] = piece.colors[2];
634 piece.colors[2] = temp;
635 }
636 }
637
638 void handleControllerButton(Uint8 button) {
639 switch (screen) {
640 case Screen::Intro:
641 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
642 setScreen(Screen::Menu);
643 }
644 break;
645 case Screen::Menu:
646 if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) {
647 menu_selection = (menu_selection + menu_item_count - 1) % menu_item_count;
648 } else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
649 menu_selection = (menu_selection + 1) % menu_item_count;
650 } else if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
651 handleMenuSelection();
652 } else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
653 exit();
654 }
655 break;
656 case Screen::Game:
657 if (button == SDL_GAMEPAD_BUTTON_BACK) {
658 setScreen(Screen::Menu);
659 } else if (button == SDL_GAMEPAD_BUTTON_START) {
660 paused = !paused;
661 } else if (paused || awaiting_name) {
662 break;
663 } else if (button == SDL_GAMEPAD_BUTTON_DPAD_LEFT) {
664 movePiece(-1, 0);
665 } else if (button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
666 movePiece(1, 0);
667 } else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
668 if (!movePiece(0, 1)) {
669 lockPiece(SDL_GetTicks());
670 }
671 } else if (button == SDL_GAMEPAD_BUTTON_DPAD_UP || button == SDL_GAMEPAD_BUTTON_SOUTH) {
672 rotatePieceColors(true);
673 } else if (button == SDL_GAMEPAD_BUTTON_EAST) {
674 rotatePieceColors(false);
675 } else if (button == SDL_GAMEPAD_BUTTON_NORTH) {
676 dropPiece();
677 }
678 break;
679 case Screen::Scores:
680 if (awaiting_name) {
681 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
682 commitScore();
683 } else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
684 score_added = true;
685 SDL_StopTextInput(window.get());
686 setScreen(Screen::Scores);
687 }
688 } else if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START || button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
689 setScreen(Screen::Menu);
690 }
691 break;
692 case Screen::Credits:
693 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START || button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
694 setScreen(Screen::Menu);
695 }
696 break;
698 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
699 commitScore();
700 } else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
701 score_added = true;
702 SDL_StopTextInput(window.get());
703 setScreen(Screen::Scores);
704 }
705 break;
706 }
707 }
708
709 void handleMenuSelection() {
710 switch (menu_selection) {
711 case 0:
712 resetGame();
713 setScreen(Screen::Game);
714 break;
715 case 1:
716 setScreen(Screen::Scores);
717 break;
718 case 2:
719 setScreen(Screen::Credits);
720 break;
721 case 3:
722 exit();
723 break;
724 default:
725 break;
726 }
727 }
728
729 void handleMenuKey(SDL_Keycode key) {
730 if (key == SDLK_ESCAPE) {
731 exit();
732 return;
733 }
734
735 if (key == SDLK_UP) {
736 menu_selection = (menu_selection + menu_item_count - 1) % menu_item_count;
737 return;
738 }
739
740 if (key == SDLK_DOWN) {
741 menu_selection = (menu_selection + 1) % menu_item_count;
742 return;
743 }
744
745 if (!isConfirmKey(key)) {
746 return;
747 }
748
749 handleMenuSelection();
750 }
751
752 void handleGameKey(SDL_Keycode key) {
753 if (key == SDLK_ESCAPE) {
754 setScreen(Screen::Menu);
755 return;
756 }
757
758 if (key == 'p' || key == 'P') {
759 paused = !paused;
760 return;
761 }
762
763 if (paused || awaiting_name) {
764 return;
765 }
766
767 if (key == SDLK_LEFT) {
768 movePiece(-1, 0);
769 } else if (key == SDLK_RIGHT) {
770 movePiece(1, 0);
771 } else if (key == SDLK_DOWN) {
772 if (!movePiece(0, 1)) {
773 lockPiece(SDL_GetTicks());
774 }
775 } else if (key == SDLK_UP) {
776 rotatePieceColors(true);
777 } else if (key == SDLK_Q) {
778 rotatePieceColors(false);
779 }
780 }
781
782 void handleNameKey(SDL_Keycode key) {
783 if (key == SDLK_ESCAPE) {
784 score_added = true;
785 SDL_StopTextInput(window.get());
786 setScreen(Screen::Scores);
787 return;
788 }
789
790 if (key == SDLK_BACKSPACE) {
791 if (!player_name.empty()) {
792 player_name.pop_back();
793 }
794 return;
795 }
796
797 if (key == SDLK_RETURN) {
798 commitScore();
799 }
800 }
801
802 void handleNameText(const char *text) {
803 if (text == nullptr) {
804 return;
805 }
806
807 while (*text != '\0' && static_cast<int>(player_name.size()) < max_name_length) {
808 const unsigned char ch = static_cast<unsigned char>(*text++);
809 if (ch >= 32 && ch < 127) {
810 player_name.push_back(static_cast<char>(ch));
811 }
812 }
813 }
814
815 void commitScore() {
816 if (!score_added) {
817 if (player_name.empty()) {
818 player_name = "Player";
819 }
820 high_scores.add(player_name, score);
821 score_added = true;
822 }
823
824 SDL_StopTextInput(window.get());
825 setScreen(Screen::Scores);
826 }
827
828 void spawnPiece() {
829 piece.colors = piece.next_colors;
830 piece.next_colors = randomColors();
831 piece.x = board_cols / 2 - 1;
832 piece.y = 0;
833
834 if (!canPlacePiece(piece.x, piece.y)) {
835 handleGameOver();
836 }
837 }
838
839 bool boardHasFlashCells() const {
840 for (const auto &row : board) {
841 for (const Cell &cell : row) {
842 if (cell.flash_until != 0U) {
843 return true;
844 }
845 }
846 }
847 return false;
848 }
849
850 bool updateFlashState(Uint64 now) {
851 if (!boardHasFlashCells()) {
852 return false;
853 }
854
855 bool expired_any = false;
856 for (auto &row : board) {
857 for (Cell &cell : row) {
858 if (cell.flash_until != 0U && now >= cell.flash_until) {
859 cell = {};
860 expired_any = true;
861 }
862 }
863 }
864
865 if (boardHasFlashCells()) {
866 return true;
867 }
868
869 if (expired_any) {
870 applyGravity();
871 resolveMatches(now);
872 }
873
874 if (!boardHasFlashCells() && waiting_for_spawn) {
875 waiting_for_spawn = false;
876 spawnPiece();
877 }
878
879 return true;
880 }
881
882 void updateGame(Uint64 now) {
883 if (paused || awaiting_name) {
884 return;
885 }
886
887 if (last_update_ms == 0U) {
888 last_update_ms = now;
889 return;
890 }
891
892 if (updateFlashState(now)) {
893 last_update_ms = now;
894 return;
895 }
896
897 if (waiting_for_spawn) {
898 waiting_for_spawn = false;
899 spawnPiece();
900 last_update_ms = now;
901 return;
902 }
903
904 const Uint64 delta = now - last_update_ms;
905 last_update_ms = now;
906 fall_accumulator_ms += delta;
907
908 while (fall_accumulator_ms >= static_cast<Uint64>(fall_delay_ms)) {
909 fall_accumulator_ms -= static_cast<Uint64>(fall_delay_ms);
910 if (!movePiece(0, 1)) {
911 lockPiece(now);
912 break;
913 }
914 }
915 }
916
917 bool resolveMatches(Uint64 now) {
918 std::array<std::array<bool, board_cols>, board_rows> marked{};
919 int matches_found = 0;
920
921 auto mark_run = [&](int x, int y, int dx, int dy) {
922 const int color = board[static_cast<std::size_t>(y)][static_cast<std::size_t>(x)].color;
923 int length = 0;
924 int cx = x;
925 int cy = y;
926
927 while (cx >= 0 && cy >= 0 && cx < board_cols && cy < board_rows && board[static_cast<std::size_t>(cy)][static_cast<std::size_t>(cx)].color == color && board[static_cast<std::size_t>(cy)][static_cast<std::size_t>(cx)].flash_until == 0U) {
928 ++length;
929 cx += dx;
930 cy += dy;
931 }
932
933 if (length < 3) {
934 return;
935 }
936
937 ++matches_found;
938 cx = x;
939 cy = y;
940 for (int i = 0; i < length; ++i) {
941 marked[static_cast<std::size_t>(cy)][static_cast<std::size_t>(cx)] = true;
942 cx += dx;
943 cy += dy;
944 }
945 };
946
947 for (int y = 0; y < board_rows; ++y) {
948 for (int x = 0; x < board_cols; ++x) {
949 const Cell &cell = board[static_cast<std::size_t>(y)][static_cast<std::size_t>(x)];
950 if (cell.color == 0 || cell.flash_until != 0U) {
951 continue;
952 }
953
954 if (x == 0 || board[static_cast<std::size_t>(y)][static_cast<std::size_t>(x - 1)].color != cell.color) {
955 mark_run(x, y, 1, 0);
956 }
957 if (y == 0 || board[static_cast<std::size_t>(y - 1)][static_cast<std::size_t>(x)].color != cell.color) {
958 mark_run(x, y, 0, 1);
959 }
960 if (x == 0 || y == 0 || board[static_cast<std::size_t>(y - 1)][static_cast<std::size_t>(x - 1)].color != cell.color) {
961 mark_run(x, y, 1, 1);
962 }
963 if (x == board_cols - 1 || y == 0 || board[static_cast<std::size_t>(y - 1)][static_cast<std::size_t>(x + 1)].color != cell.color) {
964 mark_run(x, y, -1, 1);
965 }
966 }
967 }
968
969 if (matches_found == 0) {
970 return false;
971 }
972
973 for (int y = 0; y < board_rows; ++y) {
974 for (int x = 0; x < board_cols; ++x) {
975 if (marked[static_cast<std::size_t>(y)][static_cast<std::size_t>(x)]) {
976 board[static_cast<std::size_t>(y)][static_cast<std::size_t>(x)].flash_until = now + flash_time_ms;
977 }
978 }
979 }
980
981 score += matches_found * score_points_per_match;
982 lines += matches_found;
983 lines_toward_speedup += matches_found;
984 while (lines_toward_speedup >= lines_per_speedup) {
985 lines_toward_speedup -= lines_per_speedup;
986 ++speed_level;
987 fall_delay_ms = std::max(140, 520 - speed_level * 40);
988 }
989
990 return true;
991 }
992
993 void applyGravity() {
994 for (int x = 0; x < board_cols; ++x) {
995 int write_row = board_rows - 1;
996 for (int y = board_rows - 1; y >= 0; --y) {
997 if (board[static_cast<std::size_t>(y)][static_cast<std::size_t>(x)].color != 0) {
998 if (write_row != y) {
999 board[static_cast<std::size_t>(write_row)][static_cast<std::size_t>(x)] = board[static_cast<std::size_t>(y)][static_cast<std::size_t>(x)];
1000 board[static_cast<std::size_t>(y)][static_cast<std::size_t>(x)] = {};
1001 }
1002 --write_row;
1003 }
1004 }
1005
1006 for (int y = write_row; y >= 0; --y) {
1007 board[static_cast<std::size_t>(y)][static_cast<std::size_t>(x)] = {};
1008 }
1009 }
1010 }
1011
1012 void lockPiece(Uint64 now) {
1013 if (piece.y <= 0) {
1014 handleGameOver();
1015 return;
1016 }
1017
1018 for (int i = 0; i < piece_height; ++i) {
1019 const int row = piece.y + i;
1020 board[static_cast<std::size_t>(row)][static_cast<std::size_t>(piece.x)].color = std::clamp(piece.colors[static_cast<std::size_t>(i)], 1, 9);
1021 }
1022
1023 if (resolveMatches(now)) {
1024 waiting_for_spawn = true;
1025 return;
1026 }
1027
1028 spawnPiece();
1029 }
1030
1031 void handleGameOver() {
1032 if (high_scores.qualifies(score)) {
1033 score_added = false;
1034 player_name.clear();
1035 setScreen(Screen::NameEntry);
1036 } else {
1037 setScreen(Screen::Scores);
1038 }
1039 }
1040
1041 void drawSprite(mxvk::VK_Sprite *sprite, int x, int y, int w, int h) {
1042 if (sprite != nullptr) {
1043 sprite->drawSpriteRect(x, y, w, h);
1044 }
1045 }
1046
1047 void drawCenteredText(const std::string &text, int y, const SDL_Color &color, const mxvk::Font &font) {
1048 int w = 0;
1049 int h = 0;
1050 if (!getTextDimensions(text, w, h, font)) {
1051 printText(text, scaled(32, layout.scale_x), y, color, font);
1052 return;
1053 }
1054
1055 const int x = std::max(16, (layout.width - w) / 2);
1056 printText(text, x, y, color, font);
1057 }
1058
1059 bool drawIntro(Uint64 now) {
1060 const float elapsed = intro_start_ms == 0U ? 0.0f : static_cast<float>(now - intro_start_ms) / 1000.0f;
1061 background_intro->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1062 drawSprite(background_intro, 0, 0, layout.width, layout.height);
1063
1064 if (intro_start_ms == 0U) {
1065 intro_start_ms = now;
1066 }
1067
1068 if (now - intro_start_ms > title_screen_time_ms) {
1069 setScreen(Screen::Menu);
1070 return true;
1071 }
1072
1073 return false;
1074 }
1075
1076 void drawMenu() {
1077 const float elapsed = static_cast<float>(SDL_GetTicks()) / 1000.0f;
1078 background_menu->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1079 drawSprite(background_menu, 0, 0, layout.width, layout.height);
1080
1081 for (int i = 0; i < menu_item_count; ++i) {
1082 const int y = layout.menu_y + i * layout.menu_step;
1083 if (i == menu_selection) {
1084 drawSprite(cursor, layout.menu_x - scaled(94, layout.scale_x), y + scaled(12, layout.scale_y), scaled(78, layout.scale_x), scaled(58, layout.scale_y));
1085 }
1086 drawSprite(menu_items[static_cast<std::size_t>(i)], layout.menu_x, y, layout.menu_w, layout.menu_h);
1087 }
1088 }
1089
1090 void drawScores(bool entering_name) {
1091 const float elapsed = static_cast<float>(SDL_GetTicks()) / 1000.0f;
1092 background_menu->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1093 drawSprite(background_menu, 0, 0, layout.width, layout.height);
1094 drawSprite(overlay, scaled(30, layout.scale_x), scaled(70, layout.scale_y), layout.width - scaled(60, layout.scale_x), layout.height - scaled(120, layout.scale_y));
1095
1096 drawCenteredText("High Scores", scaled(72, layout.scale_y), SDL_Color{255, 245, 200, 255}, title_font);
1097
1098 const auto &entries = high_scores.list();
1099 const int start_y = scaled(140, layout.scale_y);
1100 const int step_y = scaled(30, layout.scale_y);
1101 for (std::size_t i = 0; i < entries.size(); ++i) {
1102 const std::string line = std::format("{:>2}. {:<16} {}", i + 1, entries[i].name, entries[i].score);
1103 printText(line, scaled(70, layout.scale_x), start_y + static_cast<int>(i) * step_y, SDL_Color{255, 255, 255, 255}, ui_font);
1104 }
1105
1106 if (entering_name) {
1107 printText("Type your name and press Enter", scaled(70, layout.scale_x), scaled(450, layout.scale_y), SDL_Color{240, 220, 220, 255}, ui_font);
1108 printText("Name:", scaled(70, layout.scale_x), scaled(490, layout.scale_y), SDL_Color{255, 245, 200, 255}, ui_font);
1109 printText(player_name + "_", scaled(150, layout.scale_x), scaled(490, layout.scale_y), SDL_Color{255, 255, 255, 255}, ui_font);
1110 } else {
1111 printText("Press Enter to return to the menu", scaled(70, layout.scale_x), scaled(490, layout.scale_y), SDL_Color{240, 240, 220, 255}, ui_font);
1112 }
1113 }
1114
1115 void drawCredits() {
1116 const float elapsed = static_cast<float>(SDL_GetTicks()) / 1000.0f;
1117 background_menu->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1118 drawSprite(background_menu, 0, 0, layout.width, layout.height);
1119 drawSprite(overlay, scaled(30, layout.scale_x), scaled(85, layout.scale_y), layout.width - scaled(60, layout.scale_x), scaled(260, layout.scale_y));
1120 const int logo_w = layout.width / 2;
1121 const int logo_h = static_cast<int>(std::lround(static_cast<float>(logo_w) * static_cast<float>(mxvk_logo->getHeight()) / static_cast<float>(mxvk_logo->getWidth())));
1122 const int logo_x = (layout.width - logo_w) / 2;
1123 const int logo_y = (layout.height - logo_h) / 2;
1124 drawSprite(mxvk_logo, logo_x, logo_y, logo_w, logo_h);
1125 drawCenteredText("Credits", scaled(96, layout.scale_y), SDL_Color{255, 245, 200, 255}, title_font);
1126 printText("Original game: MasterPiece", scaled(70, layout.scale_x), scaled(180, layout.scale_y), SDL_Color{255, 255, 255, 255}, ui_font);
1127 printText("MXVK port and cleanup: Vulkan example", scaled(70, layout.scale_x), scaled(214, layout.scale_y), SDL_Color{255, 255, 255, 255}, ui_font);
1128 printText("Press Enter or Escape to return", scaled(70, layout.scale_x), scaled(270, layout.scale_y), SDL_Color{240, 240, 220, 255}, ui_font);
1129 }
1130
1131 void drawGame(Uint64 now) {
1132 const float scaleX = static_cast<float>(layout.width) / static_cast<float>(game_base_width);
1133 const float scaleY = static_cast<float>(layout.height) / static_cast<float>(game_base_height);
1134 drawSprite(background_game, 0, 0, layout.width, layout.height);
1135 drawCubeScene(now);
1136 drawHud(scaleX, scaleY);
1137
1138 if (paused) {
1139 const char *pausedText = "PAUSED - Press P to Continue";
1140 int pausedWidth = 0;
1141 int pausedHeight = 0;
1142 if (!getTextDimensions(pausedText, pausedWidth, pausedHeight, title_font)) {
1143 pausedWidth = static_cast<int>(std::strlen(pausedText)) * 16;
1144 }
1145 printText(pausedText, layout.width / 2 - pausedWidth / 2, layout.height / 2, SDL_Color{255, 255, 0, 255}, title_font);
1146 }
1147 }
1148
1149 void drawCubeScene(Uint64 now) {
1150 ensureFramebuffer();
1151 if (frame_sprite == nullptr || frame_surface == nullptr || frame_format == nullptr) {
1152 return;
1153 }
1154
1155 const float software_scale_x = static_cast<float>(frame_width) / static_cast<float>(game_base_width);
1156 const float software_scale_y = static_cast<float>(frame_height) / static_cast<float>(game_base_height);
1157 clearFrame(transparent_black);
1158 drawBoard(now, software_scale_x, software_scale_y);
1159 drawNextPiece(now, software_scale_x, software_scale_y);
1160 frame_sprite->updateTexture(frame_surface.get());
1161 frame_sprite->drawSpriteRect(0, 0, layout.width, layout.height);
1162 }
1163
1164 void updateGridViewInput(Uint64 now) {
1165 if (last_view_update_ms == 0U) {
1166 last_view_update_ms = now;
1167 return;
1168 }
1169
1170 const float delta_seconds = static_cast<float>(now - last_view_update_ms) * 0.001f;
1171 last_view_update_ms = now;
1172 if (screen != Screen::Game || awaiting_name) {
1173 return;
1174 }
1175
1176 const bool *keys = SDL_GetKeyboardState(nullptr);
1177 if (keys == nullptr) {
1178 return;
1179 }
1180
1181 if (keys[SDL_SCANCODE_A]) {
1182 grid_yaw -= GRID_YAW_SPEED * delta_seconds;
1183 }
1184 if (keys[SDL_SCANCODE_D]) {
1185 grid_yaw += GRID_YAW_SPEED * delta_seconds;
1186 }
1187 if (keys[SDL_SCANCODE_W]) {
1188 grid_pitch = std::clamp(grid_pitch + GRID_PITCH_SPEED * delta_seconds, GRID_MIN_PITCH, GRID_MAX_PITCH);
1189 }
1190 if (keys[SDL_SCANCODE_S]) {
1191 grid_pitch = std::clamp(grid_pitch - GRID_PITCH_SPEED * delta_seconds, GRID_MIN_PITCH, GRID_MAX_PITCH);
1192 }
1193 if (keys[SDL_SCANCODE_PAGEUP]) {
1194 camera_distance = std::max(GRID_MIN_CAMERA_DISTANCE, camera_distance - GRID_ZOOM_SPEED * delta_seconds);
1195 }
1196 if (keys[SDL_SCANCODE_PAGEDOWN]) {
1197 camera_distance = std::min(GRID_MAX_CAMERA_DISTANCE, camera_distance + GRID_ZOOM_SPEED * delta_seconds);
1198 }
1199 }
1200
1201 [[nodiscard]] GridCubePlacement transformGridCube(int grid_x, int grid_y, int size, float scaleX, float scaleY) const {
1202 const float cell_step_x = static_cast<float>(game_block_width + game_block_spacing) * scaleX;
1203 const float cell_step_y = static_cast<float>(game_block_height + game_block_spacing + GRID_VERTICAL_GAP) * scaleY;
1204 const float board_center_x = (static_cast<float>(game_board_start_x) + (static_cast<float>(board_cols - 1) * static_cast<float>(game_block_width + game_block_spacing) + static_cast<float>(game_block_width)) * 0.5f) * scaleX;
1205 const float board_center_y = (static_cast<float>(game_board_start_y + 10) + (static_cast<float>(board_rows - 1) * static_cast<float>(game_block_height + game_block_spacing + GRID_VERTICAL_GAP) + static_cast<float>(game_block_height)) * 0.5f) * scaleY + 10.0f;
1206 const float pitch = grid_pitch * 3.14159265358979323846f / 180.0f;
1207 const float yaw = grid_yaw * 3.14159265358979323846f / 180.0f;
1208 const float cos_pitch = std::cos(pitch);
1209 const float sin_pitch = std::sin(pitch);
1210 const float cos_yaw = std::cos(yaw);
1211 const float sin_yaw = std::sin(yaw);
1212 const float local_x = (static_cast<float>(grid_x) - (static_cast<float>(board_cols) - 1.0f) * 0.5f) * cell_step_x;
1213 const float local_y = (static_cast<float>(grid_y) - (static_cast<float>(board_rows) - 1.0f) * 0.5f) * cell_step_y;
1214 const float pitched_y = local_y * cos_pitch;
1215 const float pitched_z = local_y * sin_pitch;
1216 const float yawed_x = local_x * cos_yaw + pitched_z * sin_yaw;
1217 const float yawed_z = -local_x * sin_yaw + pitched_z * cos_yaw;
1218 const float focal_length = std::max(static_cast<float>(frame_width), static_cast<float>(frame_height)) * 1.1f;
1219 const float camera_z = focal_length * (camera_distance / GRID_DEFAULT_CAMERA_DISTANCE);
1220 const float perspective_scale = std::clamp(focal_length / std::max(1.0f, camera_z + yawed_z), 0.35f, 2.3f);
1221 return {
1222 static_cast<int>(std::lround(board_center_x + yawed_x * perspective_scale)),
1223 static_cast<int>(std::lround(board_center_y + pitched_y * perspective_scale)),
1224 std::max(4, static_cast<int>(std::lround(static_cast<float>(size) * perspective_scale))),
1225 };
1226 }
1227
1228 void drawBoard(Uint64 now, float scaleX, float scaleY) {
1229 const int cube_size = std::max(10, static_cast<int>(std::lround(static_cast<float>(game_block_height + game_block_spacing) * scaleY * 0.72f)));
1230
1231 for (int i = 0; i < board_cols; ++i) {
1232 for (int j = 0; j < board_rows; ++j) {
1233 const Cell &cell = board[static_cast<std::size_t>(j)][static_cast<std::size_t>(i)];
1234 if (cell.color == 0) {
1235 continue;
1236 }
1237
1238 const int sprite_index = cell.flash_until != 0U ? flashingSpriteIndex(i, j, now) : std::clamp(cell.color, 0, 9);
1239 const GridCubePlacement cube = transformGridCube(i, j, cube_size, scaleX, scaleY);
1240 drawCube({
1241 cube.center_x,
1242 cube.center_y,
1243 cube.size,
1244 sprite_index,
1245 static_cast<float>(i * 23 + j * 11),
1246 cell.flash_until != 0U,
1247 });
1248 }
1249 }
1250
1251 if (screen != Screen::Game) {
1252 return;
1253 }
1254
1255 for (int i = 0; i < piece_height; ++i) {
1256 const int row = piece.y + i;
1257 if (row < 0 || row >= board_rows || piece.x < 0 || piece.x >= board_cols) {
1258 continue;
1259 }
1260
1261 const GridCubePlacement cube = transformGridCube(piece.x, row, cube_size, scaleX, scaleY);
1262 drawCube({
1263 cube.center_x,
1264 cube.center_y,
1265 cube.size,
1266 std::clamp(piece.colors[static_cast<std::size_t>(i)], 0, 9),
1267 static_cast<float>(piece.x * 23 + row * 11),
1268 false,
1269 });
1270 }
1271 }
1272
1273 void drawNextPiece(Uint64 now, float scaleX, float scaleY) {
1274 const int bx = game_next_panel_x + 70;
1275 const int by = game_next_panel_y + 15;
1277 const int cube_size = std::max(10, static_cast<int>(std::lround(static_cast<float>(game_block_height + game_block_spacing) * scaleY * 0.72f)));
1278
1279 for (int i = 0; i < piece_height; ++i) {
1280 const int sprite_index = std::clamp(piece.next_colors[static_cast<std::size_t>(i)], 0, 9);
1281 drawCube({
1282 static_cast<int>(std::lround((static_cast<float>(bx) + static_cast<float>(game_block_width) * 0.5f) * scaleX)),
1283 static_cast<int>(std::lround((static_cast<float>(by + i * row_step) + static_cast<float>(game_block_height) * 0.5f) * scaleY)),
1284 cube_size,
1285 sprite_index,
1286 static_cast<float>(i * 31) + static_cast<float>(now % 1000U) * 0.01f,
1287 false,
1288 });
1289 }
1290 }
1291
1292 void ensureFramebuffer() {
1293 if (frame_surface != nullptr) {
1294 return;
1295 }
1296
1297 frame_surface = createFrameSurface(frame_width, frame_height);
1298 frame_format = SDL_GetPixelFormatDetails(frame_surface->format);
1299 if (frame_format == nullptr) {
1300 throw mxvk::Exception(std::format("Failed to query 3dmath_masterpiece frame format: {}", SDL_GetError()));
1301 }
1302
1303 clearFrame(transparent_black);
1304 frame_sprite = createSprite(frame_surface.get());
1305 frame_sprite->setTextureFilter(VK_FILTER_NEAREST);
1306 }
1307
1308 [[nodiscard]] std::uint32_t mapColor(mxvk::MXCOLOR color) const { return SDL_MapRGBA(frame_format, nullptr, mxvk::color_r(color), mxvk::color_g(color), mxvk::color_b(color), mxvk::color_a(color)); }
1309
1310 void clearFrame(mxvk::MXCOLOR color) { SDL_FillSurfaceRect(frame_surface.get(), nullptr, mapColor(color)); }
1311
1312 void putPixel(int x, int y, mxvk::MXCOLOR color) {
1313 if (x < 0 || y < 0 || x >= frame_width || y >= frame_height) {
1314 return;
1315 }
1316
1317 auto *row = static_cast<std::uint8_t *>(frame_surface->pixels) + (static_cast<std::size_t>(y) * static_cast<std::size_t>(frame_surface->pitch));
1318 auto *pixel = reinterpret_cast<std::uint32_t *>(row) + x;
1319 *pixel = mapColor(color);
1320 }
1321
1322 [[nodiscard]] static mxvk::MXCOLOR blockColor(int color) {
1323 static constexpr std::array<mxvk::MXCOLOR, 10> colors{{
1324 mxvk::MXVK_RGB(18, 18, 22),
1325 mxvk::MXVK_RGB(255, 216, 69),
1326 mxvk::MXVK_RGB(255, 140, 38),
1327 mxvk::MXVK_RGB(71, 219, 255),
1328 mxvk::MXVK_RGB(48, 98, 255),
1329 mxvk::MXVK_RGB(172, 83, 255),
1330 mxvk::MXVK_RGB(255, 104, 194),
1331 mxvk::MXVK_RGB(178, 188, 202),
1332 mxvk::MXVK_RGB(255, 64, 64),
1333 mxvk::MXVK_RGB(74, 226, 112),
1334 }};
1335 return colors[static_cast<std::size_t>(std::clamp(color, 0, 9))];
1336 }
1337
1338 [[nodiscard]] static mxvk::vec4D projectCubePoint(const mxvk::vec4D &point, const CubeInstance &cube) {
1339 const float scale = static_cast<float>(cube.size) * 2.25f;
1340 const float z = std::max(point.z, 0.001f);
1341 return {
1342 static_cast<float>(cube.center_x) + (point.x / z) * scale,
1343 static_cast<float>(cube.center_y) - (point.y / z) * scale,
1344 point.z,
1345 1.0f,
1346 };
1347 }
1348
1349 void drawCube(const CubeInstance &cube) {
1350 const float time = static_cast<float>(SDL_GetTicks()) * 0.001f;
1351 const float flash_scale = cube.flashing ? 0.72f + std::sin(time * 24.0f) * 0.28f : 1.0f;
1352 const mxvk::MXCOLOR base_color = mxvk::shade_color(blockColor(cube.color), flash_scale);
1353 const std::array<mxvk::vec4D, 8> cube_vertices = {
1354 mxvk::vec4D{-1.0f, -1.0f, -1.0f, 1.0f},
1355 mxvk::vec4D{1.0f, -1.0f, -1.0f, 1.0f},
1356 mxvk::vec4D{1.0f, 1.0f, -1.0f, 1.0f},
1357 mxvk::vec4D{-1.0f, 1.0f, -1.0f, 1.0f},
1358 mxvk::vec4D{-1.0f, -1.0f, 1.0f, 1.0f},
1359 mxvk::vec4D{1.0f, -1.0f, 1.0f, 1.0f},
1360 mxvk::vec4D{1.0f, 1.0f, 1.0f, 1.0f},
1361 mxvk::vec4D{-1.0f, 1.0f, 1.0f, 1.0f},
1362 };
1363 const std::array<std::array<int, 4>, 6> cube_faces = {{
1364 {0, 3, 2, 1},
1365 {4, 5, 6, 7},
1366 {0, 4, 7, 3},
1367 {1, 2, 6, 5},
1368 {3, 7, 6, 2},
1369 {0, 1, 5, 4},
1370 }};
1371
1372 mxvk::Mat4D rotation;
1373 rotation.BuildXYZ(0.0f, time * 112.0f + cube.phase, 0.0f);
1374
1375 std::array<mxvk::vec4D, 8> camera_vertices{};
1376 std::array<mxvk::vec4D, 8> projected{};
1377 for (std::size_t i = 0; i < cube_vertices.size(); ++i) {
1378 mxvk::vec4D point = rotation.MulVec(cube_vertices[i]);
1379 point.z += 4.2f;
1380 camera_vertices[i] = point;
1381 projected[i] = projectCubePoint(point, cube);
1382 }
1383
1384 mxvk::vec3D light_dir(-0.35f, -0.55f, -1.0f);
1385 light_dir.Normalize();
1386
1387 std::vector<FaceDraw> faces;
1388 faces.reserve(cube_faces.size());
1389 for (const auto &indices : cube_faces) {
1390 const mxvk::vec4D &a = camera_vertices[static_cast<std::size_t>(indices[0])];
1391 const mxvk::vec4D &b = camera_vertices[static_cast<std::size_t>(indices[1])];
1392 const mxvk::vec4D &c = camera_vertices[static_cast<std::size_t>(indices[2])];
1393 mxvk::vec4D normal = mxvk::vec4D().Build(a, b).CrossProduct(mxvk::vec4D().Build(a, c));
1394 normal.Normalize();
1395
1396 const mxvk::vec4D center = (a + b + c + camera_vertices[static_cast<std::size_t>(indices[3])]) * 0.25f;
1397 const mxvk::vec4D view_vector(-center.x, -center.y, -center.z, 1.0f);
1398 if (normal.DotProduct(view_vector) <= 0.0f) {
1399 continue;
1400 }
1401
1402 const float diffuse = std::max(0.0f, normal.DotProduct(mxvk::vec4D(light_dir.x, light_dir.y, light_dir.z, 1.0f)));
1403 const float intensity = std::clamp(0.28f + diffuse * 0.72f, 0.0f, 1.0f);
1404 faces.push_back({indices, center.z, mxvk::shade_color(base_color, intensity)});
1405 }
1406
1407 std::ranges::sort(faces, [](const FaceDraw &left, const FaceDraw &right) { return left.depth > right.depth; });
1408
1409 for (const FaceDraw &face : faces) {
1410 mxvk::PipeLine fill_pipeline;
1411 fill_pipeline.Begin(frame_width, frame_height, [this](int x, int y, mxvk::MXCOLOR color) { putPixel(x, y, color); });
1412 const mxvk::vec4D &a = projected[static_cast<std::size_t>(face.indices[0])];
1413 const mxvk::vec4D &b = projected[static_cast<std::size_t>(face.indices[1])];
1414 const mxvk::vec4D &c = projected[static_cast<std::size_t>(face.indices[2])];
1415 const mxvk::vec4D &d = projected[static_cast<std::size_t>(face.indices[3])];
1416 fill_pipeline.DrawFilledTriangle(a, b, c, face.color);
1417 fill_pipeline.DrawFilledTriangle(a, c, d, face.color);
1418 fill_pipeline.End();
1419 }
1420 }
1421
1422 void drawHud(float scaleX, float scaleY) {
1423 printText(std::format("Score: {}", score), static_cast<int>(200.0f * scaleX), static_cast<int>(80.0f * scaleY) - 24, SDL_Color{255, 255, 255, 255}, ui_font);
1424 printText(std::format("Tabs: {}", lines), static_cast<int>(310.0f * scaleX), static_cast<int>(80.0f * scaleY) - 24, SDL_Color{255, 255, 255, 255}, ui_font);
1425 }
1426
1427 void handleControllerAxis(Sint16 lx, Sint16 ly) {
1428 const Uint32 now = SDL_GetTicks();
1429
1430 if (screen == Screen::Menu) {
1431 if (ly < -joystick_dead_zone) {
1432 if (now - joy_repeat_up_ms > joy_repeat_delay_ms) {
1433 menu_selection = (menu_selection + menu_item_count - 1) % menu_item_count;
1434 joy_repeat_up_ms = now;
1435 }
1436 } else {
1437 joy_repeat_up_ms = 0U;
1438 }
1439
1440 if (ly > joystick_dead_zone) {
1441 if (now - joy_repeat_down_ms > joy_repeat_delay_ms) {
1442 menu_selection = (menu_selection + 1) % menu_item_count;
1443 joy_repeat_down_ms = now;
1444 }
1445 } else {
1446 joy_repeat_down_ms = 0U;
1447 }
1448 return;
1449 }
1450
1451 if (screen != Screen::Game || paused || awaiting_name) {
1452 return;
1453 }
1454
1455 if (lx < -joystick_dead_zone) {
1456 if (now - joy_repeat_left_ms > joy_repeat_delay_ms) {
1457 movePiece(-1, 0);
1458 joy_repeat_left_ms = now;
1459 }
1460 } else {
1461 joy_repeat_left_ms = 0U;
1462 }
1463
1464 if (lx > joystick_dead_zone) {
1465 if (now - joy_repeat_right_ms > joy_repeat_delay_ms) {
1466 movePiece(1, 0);
1467 joy_repeat_right_ms = now;
1468 }
1469 } else {
1470 joy_repeat_right_ms = 0U;
1471 }
1472
1473 if (ly > joystick_dead_zone) {
1474 if (now - joy_repeat_down_ms > joy_repeat_delay_ms) {
1475 if (!movePiece(0, 1)) {
1476 lockPiece(now);
1477 }
1478 joy_repeat_down_ms = now;
1479 }
1480 } else {
1481 joy_repeat_down_ms = 0U;
1482 }
1483
1484 if (lx == 0 && ly == 0) {
1485 joy_repeat_up_ms = 0U;
1486 }
1487 }
1488
1489 void pollController([[maybe_unused]] Uint64 now) {
1490 if (gamepad == nullptr) {
1491 return;
1492 }
1493
1494 const Sint16 lx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
1495 const Sint16 ly = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
1496 handleControllerAxis(lx, ly);
1497 }
1498
1499 bool openGamepad(SDL_JoystickID id) {
1500 if (gamepad != nullptr && gamepadId == id) {
1501 return true;
1502 }
1503
1504 closeGamepad();
1505 gamepad = SDL_OpenGamepad(id);
1506 if (gamepad == nullptr) {
1507 return false;
1508 }
1509
1510 gamepadId = id;
1511 return true;
1512 }
1513
1514 void tryOpenFirstGamepad() {
1515 if (gamepad != nullptr) {
1516 return;
1517 }
1518
1519 int count = 0;
1520 SDL_JoystickID *ids = SDL_GetGamepads(&count);
1521 if (ids == nullptr || count <= 0) {
1522 if (ids != nullptr) {
1523 SDL_free(ids);
1524 }
1525 return;
1526 }
1527
1528 openGamepad(ids[0]);
1529 SDL_free(ids);
1530 }
1531
1532 void closeGamepad() {
1533 if (gamepad != nullptr) {
1534 SDL_CloseGamepad(gamepad);
1535 gamepad = nullptr;
1536 }
1537 gamepadId = 0;
1538 }
1539 };
1540
1541} // namespace example
1542
1543int main(int argc, char **argv) {
1544 try {
1545 const bool explicit_resolution = hasResolutionArgument(argc, argv);
1546 Arguments args = proc_args(argc, argv);
1547 if (!explicit_resolution) {
1548 args.width = base_width;
1549 args.height = base_height;
1550 }
1551
1552 example::MasterPieceWindow window(args.path, args.width, args.height, args.fullscreen, args.enable_vsync, args.framebuffer);
1553 window.loop();
1554 } catch (mxvk::Exception &e) {
1555 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
1556 return EXIT_FAILURE;
1557 } catch (ArgException<std::string> &e) {
1558 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
1559 return EXIT_FAILURE;
1560 }
1561
1562 return EXIT_SUCCESS;
1563}
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:854
Exception thrown by Argz::proc() on unrecognised or malformed options.
Definition argz.hpp:169
const std::vector< ScoreEntry > & list() const
Definition main.cpp:171
HighScores(std::filesystem::path file_path)
Definition main.cpp:155
void add(std::string name, int score)
Definition main.cpp:157
void operator()(SDL_Surface *surface) const
Definition main.cpp:92
void event(SDL_Event &e) override
Handle one SDL event.
Definition main.cpp:308
void proc() override
Execute one processing/update step.
Definition main.cpp:365
MasterPieceWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync, const FramebufferDimensions &framebuffer)
Definition main.cpp:291
~MasterPieceWindow() override
Definition main.cpp:306
std::string text() const
void BuildXYZ(float theta_x, float theta_y, float theta_z)
Build an XYZ Euler rotation matrix from angles in degrees.
Definition mxvk_math.h:859
vec4D MulVec(const vec4D &in) const
Transform a homogeneous 4D vector by this matrix.
Definition mxvk_math.h:771
void End()
End rendering and release the current plotting callbacks.
Definition mxvk_math.h:2463
void DrawFilledTriangle(const vec2D &p0, const vec2D &p1, const vec2D &p2, MXCOLOR color) const
Draw a clipped filled triangle from 2D screen-space vertices.
Definition mxvk_math.h:2366
void Begin(int width, int height, std::function< void(int, int, MXCOLOR)> plotter)
Begin rendering with a custom pixel plotter.
Definition mxvk_math.h:2149
void updateTexture(SDL_Surface *surface)
Replace the sprite texture from an SDL_Surface.
void drawSpriteRect(int x, int y, int w, int h)
Queue a draw into an explicit destination rectangle.
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:39
VkExtent2D getSwapchainExtent() const noexcept
Get the current swapchain extent.
Definition mxvk.hpp:222
void loop()
Run the main event/render loop.
Definition mxvk.cpp:651
VK_Sprite * createSprite(const std::string &pngPath, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a sprite from a PNG file and register it with this window.
Definition mxvk.cpp:4455
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure text dimensions in pixels.
Definition mxvk.cpp:4004
std::string font_path
Definition mxvk.hpp:714
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
Definition mxvk.cpp:637
std::unique_ptr< SDL_Window, SDLWindowDeleter > window
Definition mxvk.hpp:601
void exit()
Request loop termination.
Definition mxvk.cpp:1378
VK_Window()=default
Construct an empty window object.
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
Definition mxvk.cpp:3872
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:3919
float y
Y coordinate.
Definition mxvk_math.h:369
float x
X coordinate.
Definition mxvk_math.h:366
float z
Z coordinate.
Definition mxvk_math.h:372
#define MXVK_VALIDATION
Definition mxvk.hpp:28
Math, geometry, rasterization, and simple software 3D pipeline helpers for MXVK examples.
constexpr float GRID_ZOOM_SPEED
Definition main.cpp:57
std::filesystem::path scorePath(const std::filesystem::path &asset_root)
Definition main.cpp:151
constexpr float GRID_YAW_SPEED
Definition main.cpp:55
constexpr int game_block_width
Definition main.cpp:40
constexpr int piece_height
Definition main.cpp:32
constexpr int max_name_length
Definition main.cpp:51
constexpr int game_base_height
Definition main.cpp:37
std::unique_ptr< SDL_Surface, SurfaceDeleter > SurfacePtr
Definition main.cpp:27
std::filesystem::path resolvePuzzleAssetRoot(const std::filesystem::path &asset_root)
Definition main.cpp:143
constexpr float GRID_PITCH_SPEED
Definition main.cpp:56
constexpr float GRID_MAX_PITCH
Definition main.cpp:59
constexpr int base_width
Definition main.cpp:34
constexpr int GRID_VERTICAL_GAP
Definition main.cpp:43
constexpr int menu_item_count
Definition main.cpp:46
constexpr int board_cols
Definition main.cpp:31
constexpr int game_board_start_x
Definition main.cpp:38
constexpr int flash_time_ms
Definition main.cpp:48
constexpr int game_next_panel_y
Definition main.cpp:45
constexpr int game_next_panel_x
Definition main.cpp:44
constexpr Uint32 joy_repeat_delay_ms
Definition main.cpp:52
SurfacePtr createFrameSurface(int width, int height)
Definition main.cpp:118
bool hasResolutionArgument(int argc, char **argv)
Definition main.cpp:126
constexpr int lines_per_speedup
Definition main.cpp:49
std::filesystem::path resolveAssetRoot(const std::string &path)
Definition main.cpp:136
constexpr int title_screen_time_ms
Definition main.cpp:47
constexpr float GRID_MIN_PITCH
Definition main.cpp:58
constexpr float GRID_MAX_CAMERA_DISTANCE
Definition main.cpp:61
constexpr int game_block_height
Definition main.cpp:41
constexpr float GRID_DEFAULT_CAMERA_DISTANCE
Definition main.cpp:62
constexpr int game_block_spacing
Definition main.cpp:42
constexpr int score_points_per_match
Definition main.cpp:50
constexpr float GRID_MIN_CAMERA_DISTANCE
Definition main.cpp:60
constexpr int game_base_width
Definition main.cpp:36
constexpr Sint16 joystick_dead_zone
Definition main.cpp:53
constexpr int base_height
Definition main.cpp:35
constexpr int max_scores
Definition main.cpp:33
constexpr int game_board_start_y
Definition main.cpp:39
constexpr mxvk::MXCOLOR transparent_black
Definition main.cpp:54
constexpr int board_rows
Definition main.cpp:30
int main()
Definition main.py:165
Utilities for loading and saving PNG images.
Definition mxvk.hpp:31
constexpr std::uint8_t color_r(MXCOLOR color)
Extract the red component from a packed ARGB color.
Definition mxvk_math.h:52
std::uint32_t MXCOLOR
Packed 32-bit color in ARGB byte order.
Definition mxvk_math.h:40
void BuildTables()
Rebuild the sine and cosine lookup tables.
Definition mxvk_math.h:98
MXCOLOR shade_color(MXCOLOR color, float intensity)
Scale the RGB channels of a color while preserving alpha.
Definition mxvk_math.h:69
constexpr std::uint8_t color_g(MXCOLOR color)
Extract the green component from a packed ARGB color.
Definition mxvk_math.h:55
constexpr MXCOLOR MXVK_RGB(int r, int g, int b)
Build an opaque ARGB color from red, green, and blue components.
Definition mxvk_math.h:49
constexpr std::uint8_t color_a(MXCOLOR color)
Extract the alpha component from a packed ARGB color.
Definition mxvk_math.h:61
constexpr std::uint8_t color_b(MXCOLOR color)
Extract the blue component from a packed ARGB color.
Definition mxvk_math.h:58
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:718
FramebufferDimensions framebuffer
Software framebuffer size requested by --framebuffer.
Definition argz.hpp:746
bool fullscreen
Whether fullscreen mode was requested.
Definition argz.hpp:724
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
Definition argz.hpp:738
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:721
std::string path
Asset root; proc_args() defaults it to the executable directory.
Definition argz.hpp:723
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:720
Parsed software framebuffer dimensions.
Definition argz.hpp:709
std::array< int, piece_height > next_colors
Definition main.cpp:82
std::array< int, piece_height > colors
Definition main.cpp:81