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