You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
359 lines
14 KiB
359 lines
14 KiB
#include <SDL2/SDL.h> |
|
#include <csignal> |
|
#include <iostream> |
|
#include <chrono> |
|
#ifdef MIYOO_BUILD |
|
#include <cstdlib> |
|
#include <fcntl.h> |
|
#include <unistd.h> |
|
#include <sys/ioctl.h> |
|
#include <linux/fb.h> |
|
#endif |
|
#include "Game.hpp" |
|
#include "Synth.hpp" |
|
#include "Renderer.hpp" |
|
|
|
// Crash signal handler to print diagnostic trace to cybermatris.log |
|
void crashHandler(int signum) { |
|
std::cerr << "\n\n==================================================" << std::endl; |
|
std::cerr << "[CRASH DETECTED] CyberMatris caught fatal signal " << signum; |
|
switch (signum) { |
|
case SIGSEGV: std::cerr << " (SIGSEGV: Segmentation Fault)"; break; |
|
case SIGABRT: std::cerr << " (SIGABRT: Abort / Assertion failed)"; break; |
|
case SIGFPE: std::cerr << " (SIGFPE: Floating Point Exception)"; break; |
|
case SIGILL: std::cerr << " (SIGILL: Illegal Instruction)"; break; |
|
case SIGBUS: std::cerr << " (SIGBUS: Bus Error)"; break; |
|
default: std::cerr << " (Unknown Signal)"; break; |
|
} |
|
std::cerr << "\n==================================================" << std::endl; |
|
std::cerr.flush(); |
|
|
|
// Restore default handler and re-raise to complete execution termination |
|
std::signal(signum, SIG_DFL); |
|
std::raise(signum); |
|
} |
|
|
|
int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { |
|
// Register crash signal handlers |
|
std::signal(SIGSEGV, crashHandler); |
|
std::signal(SIGABRT, crashHandler); |
|
std::signal(SIGFPE, crashHandler); |
|
std::signal(SIGILL, crashHandler); |
|
std::signal(SIGBUS, crashHandler); |
|
|
|
// 1. Initialize SDL2 subsystems |
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) { |
|
std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << std::endl; |
|
return -1; |
|
} |
|
|
|
// 2. Create display window |
|
int win_w = 800; |
|
int win_h = 600; |
|
Uint32 win_flags = SDL_WINDOW_SHOWN; |
|
|
|
#ifdef MIYOO_BUILD |
|
win_flags |= SDL_WINDOW_FULLSCREEN; |
|
|
|
// Default fallback |
|
win_w = 640; |
|
win_h = 480; |
|
|
|
// 1. Try environment variables first |
|
const char* env_w = std::getenv("MIYOO_SCREEN_WIDTH"); |
|
const char* env_h = std::getenv("MIYOO_SCREEN_HEIGHT"); |
|
if (env_w && env_h) { |
|
int parsed_w = std::atoi(env_w); |
|
int parsed_h = std::atoi(env_h); |
|
if (parsed_w > 0 && parsed_h > 0) { |
|
win_w = parsed_w; |
|
win_h = parsed_h; |
|
std::cout << "[INFO] Detected screen resolution from environment: " << win_w << "x" << win_h << std::endl; |
|
} |
|
} else { |
|
// 2. Fallback to /dev/fb0 ioctl |
|
int fd = open("/dev/fb0", O_RDONLY); |
|
if (fd >= 0) { |
|
struct fb_var_screeninfo vinfo; |
|
if (ioctl(fd, FBIOGET_VSCREENINFO, &vinfo) >= 0) { |
|
if (vinfo.xres > 0 && vinfo.yres > 0) { |
|
win_w = vinfo.xres; |
|
win_h = vinfo.yres; |
|
std::cout << "[INFO] Detected screen resolution from /dev/fb0: " << win_w << "x" << win_h << std::endl; |
|
} |
|
} |
|
close(fd); |
|
} else { |
|
std::cerr << "[WARNING] Failed to open /dev/fb0 for resolution query, using default 640x480" << std::endl; |
|
} |
|
} |
|
#endif |
|
|
|
SDL_Window* window = SDL_CreateWindow( |
|
"CyberMatris - Programmatic Retro Tetris", |
|
SDL_WINDOWPOS_CENTERED, |
|
SDL_WINDOWPOS_CENTERED, |
|
win_w, win_h, |
|
win_flags |
|
); |
|
if (window == nullptr) { |
|
std::cerr << "Window could not be created! SDL_Error: " << SDL_GetError() << std::endl; |
|
SDL_Quit(); |
|
return -1; |
|
} |
|
|
|
// 3. Create GPU Hardware Accelerated Renderer with VSync |
|
SDL_Renderer* renderer = SDL_CreateRenderer( |
|
window, -1, |
|
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC |
|
); |
|
#ifdef MIYOO_BUILD |
|
if (renderer != nullptr) { |
|
std::cerr << "[MIYOO] Hardware accelerated renderer OK" << std::endl; |
|
} |
|
#endif |
|
if (renderer == nullptr) { |
|
std::cerr << "Renderer could not be created! SDL_Error: " << SDL_GetError() << std::endl; |
|
SDL_DestroyWindow(window); |
|
SDL_Quit(); |
|
return -1; |
|
} |
|
|
|
// 4. Initialize Procedural Audio Synthesizer |
|
Synth synth; |
|
if (!synth.init()) { |
|
std::cerr << "Warning: Could not initialize programmatic audio synthesizer! SDL_Error: " << SDL_GetError() << std::endl; |
|
} |
|
|
|
// 5. Initialize Game Logic and Renderer |
|
Game game; |
|
Renderer gameRenderer; |
|
if (!gameRenderer.init(renderer, win_w, win_h)) { |
|
std::cerr << "Failed to build procedural graphics textures!" << std::endl; |
|
SDL_DestroyRenderer(renderer); |
|
SDL_DestroyWindow(window); |
|
SDL_Quit(); |
|
return -1; |
|
} |
|
|
|
bool quit = false; |
|
SDL_Event event; |
|
|
|
// Track frame timing using high-precision performance counters |
|
uint64_t lastTime = SDL_GetPerformanceCounter(); |
|
double freq = static_cast<double>(SDL_GetPerformanceFrequency()); |
|
|
|
float softDropTimer = 0.0f; |
|
|
|
// Flush initial queued events (e.g., from OnionOS menu launch/button presses) |
|
std::cout << "[INFO] Flushing initial event queue..." << std::endl; |
|
SDL_Event trashEvent; |
|
int flushedCount = 0; |
|
while (SDL_PollEvent(&trashEvent)) { |
|
flushedCount++; |
|
} |
|
std::cout << "[INFO] Flushed " << flushedCount << " initial events." << std::endl; |
|
|
|
// 6. Execution Loop |
|
while (!quit) { |
|
// A. Calculate frame delta-time (dt) |
|
uint64_t currentTime = SDL_GetPerformanceCounter(); |
|
float dt = static_cast<float>((currentTime - lastTime) / freq); |
|
lastTime = currentTime; |
|
|
|
// Cap dt to prevent massive jumps during window drags or lags |
|
if (dt > 0.1f) dt = 0.1f; |
|
|
|
// B. Handle Inputs & Events |
|
while (SDL_PollEvent(&event) != 0) { |
|
if (event.type == SDL_QUIT) { |
|
std::cout << "[EVENT] Received SDL_QUIT signal." << std::endl; |
|
quit = true; |
|
} else if (event.type == SDL_KEYDOWN) { |
|
SDL_Keycode sym = event.key.keysym.sym; |
|
std::cout << "[EVENT] Key Down: " << sym << " (" << SDL_GetKeyName(sym) << ")" << std::endl; |
|
|
|
if (sym == SDLK_s) { |
|
gameRenderer.takeScreenshot("screenshot.bmp"); |
|
std::cout << "[INFO] Screenshot captured to screenshot.bmp" << std::endl; |
|
} |
|
|
|
// START Screen Inputs |
|
if (game.getState() == GameState::START) { |
|
if (sym == SDLK_RETURN || sym == SDLK_KP_ENTER || sym == SDLK_LALT) { |
|
std::cout << "[INPUT] Start/Confirm game triggered." << std::endl; |
|
game.setState(GameState::PLAYING); |
|
game.reset(); |
|
synth.playBGM(true); |
|
} else if (sym == SDLK_ESCAPE) { |
|
// Cooldown check (1000ms) to prevent immediate exit due to leftover events on boot |
|
Uint32 currentTicks = SDL_GetTicks(); |
|
if (currentTicks > 1000) { |
|
std::cout << "[INPUT] Quit game requested via ESCAPE/SELECT (ticks: " << currentTicks << ")" << std::endl; |
|
quit = true; |
|
} else { |
|
std::cout << "[INPUT] Blocked early ESCAPE/SELECT quit request (cooldown ticks: " << currentTicks << ")" << std::endl; |
|
} |
|
} |
|
} |
|
// PLAYING State Inputs |
|
else if (game.getState() == GameState::PLAYING) { |
|
switch (sym) { |
|
case SDLK_LEFT: |
|
game.moveLeft(); |
|
break; |
|
case SDLK_RIGHT: |
|
game.moveRight(); |
|
break; |
|
case SDLK_UP: |
|
case SDLK_x: |
|
case SDLK_LALT: // Gamepad A (Rotate CW) |
|
game.rotate(1); // Clockwise rotation |
|
break; |
|
case SDLK_z: |
|
case SDLK_LCTRL: // Gamepad B (Rotate CCW) |
|
game.rotate(-1); // Counter-Clockwise rotation |
|
break; |
|
case SDLK_SPACE: |
|
game.hardDrop(); |
|
break; |
|
case SDLK_c: |
|
case SDLK_LSHIFT: |
|
game.holdPiece(); |
|
break; |
|
case SDLK_p: |
|
game.setState(GameState::PAUSED); |
|
synth.playBGM(false); |
|
break; |
|
case SDLK_r: |
|
synth.stopAllSFX(); |
|
game.reset(); |
|
synth.playBGM(true); |
|
break; |
|
case SDLK_ESCAPE: |
|
synth.stopAllSFX(); |
|
game.setState(GameState::START); |
|
synth.playBGM(false); |
|
break; |
|
default: |
|
break; |
|
} |
|
} |
|
// PAUSED State Inputs |
|
else if (game.getState() == GameState::PAUSED) { |
|
if (sym == SDLK_p) { |
|
game.setState(GameState::PLAYING); |
|
synth.playBGM(true); |
|
} else if (sym == SDLK_ESCAPE) { |
|
synth.stopAllSFX(); |
|
game.setState(GameState::START); |
|
synth.playBGM(false); |
|
} |
|
} |
|
// GAME OVER State Inputs |
|
else if (game.getState() == GameState::GAME_OVER) { |
|
if (sym == SDLK_r) { |
|
synth.stopAllSFX(); |
|
game.setState(GameState::PLAYING); |
|
game.reset(); |
|
synth.playBGM(true); |
|
} else if (sym == SDLK_ESCAPE || sym == SDLK_RETURN || sym == SDLK_KP_ENTER || sym == SDLK_LALT) { |
|
synth.stopAllSFX(); |
|
game.setState(GameState::START); |
|
synth.playBGM(false); |
|
} |
|
} |
|
} |
|
} |
|
|
|
// C. Continuous Keyboard Polling for Smooth Soft-Drop sliding |
|
if (game.getState() == GameState::PLAYING) { |
|
const Uint8* keyboardState = SDL_GetKeyboardState(nullptr); |
|
if (keyboardState[SDL_SCANCODE_DOWN]) { |
|
softDropTimer += dt; |
|
if (softDropTimer >= 0.04f) { // Soft drop tick speed (every 40ms) |
|
softDropTimer = 0.0f; |
|
game.softDrop(); |
|
} |
|
} else { |
|
softDropTimer = 0.0f; |
|
} |
|
} |
|
|
|
// D. Update Game Logic (gravity timing) |
|
bool lineCleared = game.update(dt); |
|
if (lineCleared) { |
|
// Trigger glorious pixel explosion of sparks on cleared rows |
|
gameRenderer.spawnLineClearParticles(game); |
|
} |
|
|
|
// E. Bridge Game Events to Synth Sound Triggers and Particle Emitters |
|
if (game.flagMoveSFX) { |
|
synth.triggerSFX(SFXType::MOVE); |
|
game.flagMoveSFX = false; |
|
} |
|
if (game.flagRotateSFX) { |
|
synth.triggerSFX(SFXType::ROTATE); |
|
game.flagRotateSFX = false; |
|
} |
|
if (game.flagLandSFX) { |
|
synth.triggerSFX(SFXType::LAND); |
|
|
|
// Spawn subtle white-gray impact smoke clouds at bottom row of contact |
|
auto lockedCells = game.mLastLockedCells; |
|
if (!lockedCells.empty()) { |
|
int lowestY = -100; |
|
for (const auto& p : lockedCells) { |
|
if (p.y > lowestY) lowestY = p.y; |
|
} |
|
for (const auto& p : lockedCells) { |
|
if (p.y == lowestY) { |
|
gameRenderer.spawnLandDustParticles(p.x, p.y, game.getActivePieceType()); |
|
} |
|
} |
|
} |
|
game.flagLandSFX = false; |
|
} |
|
if (game.flagLineClearSFX) { |
|
synth.triggerSFX(SFXType::LINE_CLEAR); |
|
game.flagLineClearSFX = false; |
|
} |
|
if (game.flagTetrisClearSFX) { |
|
synth.triggerSFX(SFXType::TETRIS_CLEAR); |
|
game.flagTetrisClearSFX = false; |
|
} |
|
if (game.flagLevelUpSFX) { |
|
synth.triggerSFX(SFXType::LEVEL_UP); |
|
game.flagLevelUpSFX = false; |
|
} |
|
if (game.flagGameOverSFX) { |
|
synth.triggerSFX(SFXType::GAME_OVER); |
|
synth.playBGM(false); |
|
game.flagGameOverSFX = false; |
|
} |
|
|
|
// F. Update Parallax Background and Particle physics |
|
gameRenderer.update(dt); |
|
|
|
// G. Render active frame (automatically uploads backbuffer and calls SDL_RenderPresent) |
|
gameRenderer.render(game, synth); |
|
|
|
// H. Automatic periodic screenshot disabled on Miyoo (FAT32 write-during-render issues) |
|
#ifndef MIYOO_BUILD |
|
static float autoScreenshotTimer = 0.0f; |
|
autoScreenshotTimer += dt; |
|
if (autoScreenshotTimer >= 2.0f) { |
|
autoScreenshotTimer = 0.0f; |
|
gameRenderer.takeScreenshot("screenshot.bmp"); |
|
} |
|
#endif |
|
} |
|
|
|
// 7. Cleanup and close resources |
|
synth.playBGM(false); |
|
SDL_DestroyRenderer(renderer); |
|
SDL_DestroyWindow(window); |
|
SDL_Quit(); |
|
|
|
return 0; |
|
}
|
|
|