Add FPS counter, VSync toggle, shader metadata system, and multi-pass infrastructure
- FPS counter in window title (updates every 500ms) - F4 key toggles VSync on/off - Shader metadata: Name and Author from comments - iChannel metadata parsing for multi-pass support - Base structures: ShaderBuffer, ShaderPass - FBO/texture management functions - Updated all 11 shaders with Name/Author metadata 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
353
src/main.cpp
353
src/main.cpp
@@ -7,8 +7,10 @@
|
||||
#include <algorithm>
|
||||
#include <type_traits>
|
||||
#include <filesystem>
|
||||
#include <ctime>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <glad/glad.h>
|
||||
#include "jail_audio.h"
|
||||
|
||||
#include "defines.hpp"
|
||||
|
||||
@@ -21,6 +23,7 @@ struct Logger {
|
||||
// Opciones mínimas parecidas a las tuyas
|
||||
struct VideoOptions {
|
||||
bool fullscreen = false;
|
||||
bool vsync = true;
|
||||
} Options_video;
|
||||
|
||||
// Estructura para guardar info del display
|
||||
@@ -35,13 +38,30 @@ struct DisplayMonitor {
|
||||
static DisplayMonitor display_monitor_;
|
||||
static SDL_Window* window_ = nullptr;
|
||||
|
||||
// Sistema de shaders
|
||||
// Sistema de shaders (legacy - kept for backward compatibility with single-pass shaders)
|
||||
static std::vector<std::filesystem::path> shader_list_;
|
||||
static std::vector<std::string> shader_names_; // Custom names from "// Name: XXX" comments
|
||||
static std::vector<std::string> shader_authors_; // Custom authors from "// Author: XXX" comments
|
||||
static size_t current_shader_index_ = 0;
|
||||
static std::filesystem::path shaders_directory_;
|
||||
static GLuint current_program_ = 0;
|
||||
static Uint32 shader_start_ticks_ = 0;
|
||||
|
||||
// Multi-pass shader system
|
||||
static std::vector<ShaderPass> shader_passes_;
|
||||
static int current_window_width_ = 0;
|
||||
static int current_window_height_ = 0;
|
||||
|
||||
// FPS tracking
|
||||
static Uint32 fps_frame_count_ = 0;
|
||||
static Uint32 fps_last_update_ticks_ = 0;
|
||||
static float current_fps_ = 0.0f;
|
||||
|
||||
// Sistema de música
|
||||
static std::vector<std::filesystem::path> music_list_;
|
||||
static size_t current_music_index_ = 0;
|
||||
static JA_Music_t* current_music_ = nullptr;
|
||||
|
||||
// Vertex shader embebido
|
||||
static const char* vertexShaderSrc = R"glsl(
|
||||
#version 330 core
|
||||
@@ -63,6 +83,89 @@ static bool loadFileToString(const std::filesystem::path& path, std::string& out
|
||||
return true;
|
||||
}
|
||||
|
||||
struct ShaderMetadata {
|
||||
std::string name;
|
||||
std::string author;
|
||||
std::string iChannel0; // "BufferA", "BufferB", "none", etc.
|
||||
std::string iChannel1;
|
||||
std::string iChannel2;
|
||||
std::string iChannel3;
|
||||
};
|
||||
|
||||
struct ShaderBuffer {
|
||||
GLuint program = 0; // Shader program for this buffer
|
||||
GLuint fbo = 0; // Framebuffer object
|
||||
GLuint texture = 0; // Output texture
|
||||
std::string name; // "BufferA", "BufferB", etc.
|
||||
};
|
||||
|
||||
struct ShaderPass {
|
||||
std::string shaderName; // Base name (e.g., "water")
|
||||
std::string displayName; // Custom name from metadata
|
||||
std::string author; // Author from metadata
|
||||
GLuint imageProgram = 0; // Main image shader program
|
||||
std::vector<ShaderBuffer> buffers; // BufferA, BufferB, etc.
|
||||
ShaderMetadata metadata; // iChannel configuration
|
||||
};
|
||||
|
||||
static std::string trimString(const std::string& str) {
|
||||
size_t start = str.find_first_not_of(" \t\r\n");
|
||||
size_t end = str.find_last_not_of(" \t\r\n");
|
||||
if (start != std::string::npos && end != std::string::npos) {
|
||||
return str.substr(start, end - start + 1);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static ShaderMetadata extractShaderMetadata(const std::string& shaderSource) {
|
||||
ShaderMetadata metadata;
|
||||
metadata.iChannel0 = "none";
|
||||
metadata.iChannel1 = "none";
|
||||
metadata.iChannel2 = "none";
|
||||
metadata.iChannel3 = "none";
|
||||
|
||||
std::istringstream stream(shaderSource);
|
||||
std::string line;
|
||||
int lineCount = 0;
|
||||
const int maxLinesToCheck = 30;
|
||||
|
||||
while (std::getline(stream, line) && lineCount < maxLinesToCheck) {
|
||||
lineCount++;
|
||||
|
||||
// Look for "// XXX: YYY" patterns (case-insensitive)
|
||||
size_t pos = line.find("//");
|
||||
if (pos != std::string::npos) {
|
||||
std::string comment = line.substr(pos + 2);
|
||||
std::string commentLower = comment;
|
||||
std::transform(commentLower.begin(), commentLower.end(), commentLower.begin(), ::tolower);
|
||||
|
||||
// Check for Name:
|
||||
if (commentLower.find("name:") != std::string::npos) {
|
||||
metadata.name = trimString(comment.substr(comment.find(":") + 1));
|
||||
}
|
||||
// Check for Author:
|
||||
else if (commentLower.find("author:") != std::string::npos) {
|
||||
metadata.author = trimString(comment.substr(comment.find(":") + 1));
|
||||
}
|
||||
// Check for iChannel0-3:
|
||||
else if (commentLower.find("ichannel0:") != std::string::npos) {
|
||||
metadata.iChannel0 = trimString(comment.substr(comment.find(":") + 1));
|
||||
}
|
||||
else if (commentLower.find("ichannel1:") != std::string::npos) {
|
||||
metadata.iChannel1 = trimString(comment.substr(comment.find(":") + 1));
|
||||
}
|
||||
else if (commentLower.find("ichannel2:") != std::string::npos) {
|
||||
metadata.iChannel2 = trimString(comment.substr(comment.find(":") + 1));
|
||||
}
|
||||
else if (commentLower.find("ichannel3:") != std::string::npos) {
|
||||
metadata.iChannel3 = trimString(comment.substr(comment.find(":") + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
static std::vector<std::filesystem::path> scanShaderDirectory(const std::filesystem::path& directory) {
|
||||
std::vector<std::filesystem::path> shaders;
|
||||
|
||||
@@ -84,14 +187,168 @@ static std::vector<std::filesystem::path> scanShaderDirectory(const std::filesys
|
||||
std::sort(shaders.begin(), shaders.end());
|
||||
|
||||
Logger::info("Found " + std::to_string(shaders.size()) + " shader(s) in " + directory.string());
|
||||
|
||||
// Initialize shader metadata vectors with empty strings (will be filled when shaders are loaded)
|
||||
shader_names_.resize(shaders.size(), "");
|
||||
shader_authors_.resize(shaders.size(), "");
|
||||
|
||||
return shaders;
|
||||
}
|
||||
|
||||
static std::vector<std::filesystem::path> scanMusicDirectory(const std::filesystem::path& directory) {
|
||||
std::vector<std::filesystem::path> music_files;
|
||||
|
||||
if (!std::filesystem::exists(directory) || !std::filesystem::is_directory(directory)) {
|
||||
Logger::info("Music directory does not exist: " + directory.string());
|
||||
return music_files;
|
||||
}
|
||||
|
||||
for (const auto& entry : std::filesystem::directory_iterator(directory)) {
|
||||
if (entry.is_regular_file()) {
|
||||
auto ext = entry.path().extension().string();
|
||||
if (ext == ".ogg") {
|
||||
music_files.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ordenar alfabéticamente
|
||||
std::sort(music_files.begin(), music_files.end());
|
||||
|
||||
Logger::info("Found " + std::to_string(music_files.size()) + " music file(s) in " + directory.string());
|
||||
return music_files;
|
||||
}
|
||||
|
||||
static void playRandomMusic() {
|
||||
if (music_list_.empty()) return;
|
||||
|
||||
// Liberar música anterior si existe
|
||||
if (current_music_) {
|
||||
JA_DeleteMusic(current_music_);
|
||||
current_music_ = nullptr;
|
||||
}
|
||||
|
||||
// Elegir índice aleatorio
|
||||
current_music_index_ = rand() % music_list_.size();
|
||||
|
||||
// Cargar y reproducir música (sin loop, loop=0)
|
||||
const auto& music_path = music_list_[current_music_index_];
|
||||
current_music_ = JA_LoadMusic(music_path.string().c_str());
|
||||
|
||||
if (current_music_) {
|
||||
JA_PlayMusic(current_music_, 0); // 0 = no loop, se reproduce una vez
|
||||
Logger::info("Now playing: " + music_path.filename().string());
|
||||
} else {
|
||||
Logger::error("Failed to load music: " + music_path.string());
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Multi-pass FBO/Texture Management =====
|
||||
|
||||
static bool createBufferFBO(ShaderBuffer& buffer, int width, int height) {
|
||||
// Create texture
|
||||
glGenTextures(1, &buffer.texture);
|
||||
glBindTexture(GL_TEXTURE_2D, buffer.texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
// Create FBO
|
||||
glGenFramebuffers(1, &buffer.fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, buffer.fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, buffer.texture, 0);
|
||||
|
||||
// Check FBO completeness
|
||||
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
Logger::error("FBO creation failed for " + buffer.name + ": " + std::to_string(status));
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger::info("Created FBO for " + buffer.name + " (" + std::to_string(width) + "x" + std::to_string(height) + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
static void destroyBuffer(ShaderBuffer& buffer) {
|
||||
if (buffer.fbo != 0) {
|
||||
glDeleteFramebuffers(1, &buffer.fbo);
|
||||
buffer.fbo = 0;
|
||||
}
|
||||
if (buffer.texture != 0) {
|
||||
glDeleteTextures(1, &buffer.texture);
|
||||
buffer.texture = 0;
|
||||
}
|
||||
if (buffer.program != 0) {
|
||||
glDeleteProgram(buffer.program);
|
||||
buffer.program = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void destroyShaderPass(ShaderPass& pass) {
|
||||
if (pass.imageProgram != 0) {
|
||||
glDeleteProgram(pass.imageProgram);
|
||||
pass.imageProgram = 0;
|
||||
}
|
||||
for (auto& buffer : pass.buffers) {
|
||||
destroyBuffer(buffer);
|
||||
}
|
||||
pass.buffers.clear();
|
||||
}
|
||||
|
||||
static bool resizeBuffersIfNeeded(ShaderPass& pass, int width, int height) {
|
||||
if (current_window_width_ == width && current_window_height_ == height) {
|
||||
return false; // No resize needed
|
||||
}
|
||||
|
||||
Logger::info("Resizing buffers: " + std::to_string(width) + "x" + std::to_string(height));
|
||||
|
||||
// Destroy and recreate all buffers with new size
|
||||
for (auto& buffer : pass.buffers) {
|
||||
// Keep program, destroy FBO/texture only
|
||||
if (buffer.fbo != 0) glDeleteFramebuffers(1, &buffer.fbo);
|
||||
if (buffer.texture != 0) glDeleteTextures(1, &buffer.texture);
|
||||
buffer.fbo = 0;
|
||||
buffer.texture = 0;
|
||||
|
||||
if (!createBufferFBO(buffer, width, height)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
current_window_width_ = width;
|
||||
current_window_height_ = height;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void updateWindowTitle() {
|
||||
if (!window_ || shader_list_.empty()) return;
|
||||
|
||||
std::string filename = shader_list_[current_shader_index_].filename().string();
|
||||
std::string title = std::string(APP_NAME) + " (" + filename + ")";
|
||||
// Use custom shader name if available, otherwise fallback to filename
|
||||
std::string shaderName;
|
||||
if (!shader_names_.empty() && !shader_names_[current_shader_index_].empty()) {
|
||||
shaderName = shader_names_[current_shader_index_];
|
||||
} else {
|
||||
shaderName = shader_list_[current_shader_index_].filename().string();
|
||||
}
|
||||
|
||||
// Add author if available
|
||||
if (!shader_authors_.empty() && !shader_authors_[current_shader_index_].empty()) {
|
||||
shaderName += " by " + shader_authors_[current_shader_index_];
|
||||
}
|
||||
|
||||
std::string title = std::string(APP_NAME) + " (" + shaderName + ")";
|
||||
|
||||
if (current_fps_ > 0.0f) {
|
||||
title += " - " + std::to_string(static_cast<int>(current_fps_ + 0.5f)) + " FPS";
|
||||
}
|
||||
|
||||
title += Options_video.vsync ? " [VSync ON]" : " [VSync OFF]";
|
||||
|
||||
SDL_SetWindowTitle(window_, title.c_str());
|
||||
}
|
||||
|
||||
@@ -147,6 +404,17 @@ static GLuint loadAndCompileShader(size_t index) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Extract custom shader metadata (name and author) from source code
|
||||
ShaderMetadata metadata = extractShaderMetadata(fragSrc);
|
||||
if (!metadata.name.empty()) {
|
||||
shader_names_[index] = metadata.name;
|
||||
Logger::info("Shader name: " + metadata.name);
|
||||
}
|
||||
if (!metadata.author.empty()) {
|
||||
shader_authors_[index] = metadata.author;
|
||||
Logger::info("Shader author: " + metadata.author);
|
||||
}
|
||||
|
||||
GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSrc);
|
||||
GLuint fs = compileShader(GL_FRAGMENT_SHADER, fragSrc.c_str());
|
||||
|
||||
@@ -208,11 +476,15 @@ void setFullscreenMode() {
|
||||
SDL_SetWindowFullscreen(window_, false);
|
||||
SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||
Options_video.fullscreen = false;
|
||||
SDL_ShowCursor(); // Show cursor on fallback to windowed
|
||||
} else {
|
||||
SDL_HideCursor(); // Hide cursor in fullscreen
|
||||
}
|
||||
} else {
|
||||
// Volver a modo ventana 800x800
|
||||
SDL_SetWindowFullscreen(window_, false);
|
||||
SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||
SDL_ShowCursor(); // Show cursor in windowed mode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,6 +493,17 @@ void toggleFullscreen() {
|
||||
setFullscreenMode();
|
||||
}
|
||||
|
||||
void toggleVSync() {
|
||||
Options_video.vsync = !Options_video.vsync;
|
||||
int result = SDL_GL_SetSwapInterval(Options_video.vsync ? 1 : 0);
|
||||
|
||||
if (result == 0) {
|
||||
Logger::info(Options_video.vsync ? "VSync enabled" : "VSync disabled");
|
||||
} else {
|
||||
Logger::error(std::string("Failed to set VSync: ") + SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
void switchShader(int direction) {
|
||||
if (shader_list_.empty()) return;
|
||||
|
||||
@@ -260,6 +543,10 @@ void handleDebugEvents(const SDL_Event& event) {
|
||||
toggleFullscreen();
|
||||
break;
|
||||
}
|
||||
case SDLK_F4: {
|
||||
toggleVSync();
|
||||
break;
|
||||
}
|
||||
case SDLK_LEFT: {
|
||||
switchShader(-1);
|
||||
break;
|
||||
@@ -287,7 +574,7 @@ int main(int argc, char** argv) {
|
||||
Options_video.fullscreen = fullscreenFlag;
|
||||
|
||||
// Inicializar SDL3
|
||||
auto initResult = SDL_Init(SDL_INIT_VIDEO);
|
||||
auto initResult = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
|
||||
if constexpr (std::is_same_v<decltype(initResult), bool>) {
|
||||
if (!initResult) { Logger::error(SDL_GetError()); return -1; }
|
||||
} else {
|
||||
@@ -326,9 +613,34 @@ int main(int argc, char** argv) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Set initial vsync state
|
||||
int vsync_result = SDL_GL_SetSwapInterval(Options_video.vsync ? 1 : 0);
|
||||
if (vsync_result == 0) {
|
||||
Logger::info(Options_video.vsync ? "VSync enabled" : "VSync disabled");
|
||||
} else {
|
||||
Logger::error(std::string("Failed to set initial VSync: ") + SDL_GetError());
|
||||
}
|
||||
|
||||
// Inicializar jail_audio
|
||||
JA_Init(48000, SDL_AUDIO_S16LE, 2);
|
||||
|
||||
// Obtener directorio de recursos
|
||||
std::string resources_dir = getResourcesDirectory();
|
||||
|
||||
// Inicializar generador de números aleatorios
|
||||
srand(static_cast<unsigned int>(time(nullptr)));
|
||||
|
||||
// Escanear directorio de música
|
||||
std::filesystem::path music_directory = std::filesystem::path(resources_dir) / "data" / "music";
|
||||
music_list_ = scanMusicDirectory(music_directory);
|
||||
|
||||
// Reproducir primera canción aleatoria
|
||||
if (!music_list_.empty()) {
|
||||
playRandomMusic();
|
||||
} else {
|
||||
Logger::info("No music files found in " + music_directory.string());
|
||||
}
|
||||
|
||||
// Determinar carpeta de shaders
|
||||
std::filesystem::path shaderFile(shaderPath);
|
||||
if (shaderFile.has_parent_path()) {
|
||||
@@ -391,6 +703,7 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
|
||||
shader_start_ticks_ = SDL_GetTicks();
|
||||
fps_last_update_ticks_ = SDL_GetTicks();
|
||||
updateWindowTitle();
|
||||
|
||||
// Quad setup
|
||||
@@ -415,6 +728,27 @@ int main(int argc, char** argv) {
|
||||
bool running = true;
|
||||
|
||||
while (running) {
|
||||
// Update FPS counter
|
||||
fps_frame_count_++;
|
||||
Uint32 current_ticks = SDL_GetTicks();
|
||||
|
||||
// Update FPS display every 500ms
|
||||
if (current_ticks - fps_last_update_ticks_ >= 500) {
|
||||
float elapsed_seconds = (current_ticks - fps_last_update_ticks_) / 1000.0f;
|
||||
current_fps_ = fps_frame_count_ / elapsed_seconds;
|
||||
fps_frame_count_ = 0;
|
||||
fps_last_update_ticks_ = current_ticks;
|
||||
updateWindowTitle();
|
||||
}
|
||||
|
||||
// Actualizar audio (necesario para streaming y loops)
|
||||
JA_Update();
|
||||
|
||||
// Verificar si la música actual terminó y reproducir siguiente aleatoria
|
||||
if (!music_list_.empty() && JA_GetMusicState() == JA_MUSIC_STOPPED) {
|
||||
playRandomMusic();
|
||||
}
|
||||
|
||||
SDL_Event e;
|
||||
while (SDL_PollEvent(&e)) {
|
||||
if (e.type == SDL_EVENT_QUIT) running = false;
|
||||
@@ -452,7 +786,9 @@ int main(int argc, char** argv) {
|
||||
glBindVertexArray(0);
|
||||
|
||||
SDL_GL_SwapWindow(window_);
|
||||
SDL_Delay(1);
|
||||
if (!Options_video.vsync) {
|
||||
SDL_Delay(1); // Prevent CPU spinning when vsync is off
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
@@ -462,6 +798,13 @@ int main(int argc, char** argv) {
|
||||
glDeleteProgram(current_program_);
|
||||
}
|
||||
|
||||
// Cleanup audio
|
||||
if (current_music_) {
|
||||
JA_DeleteMusic(current_music_);
|
||||
current_music_ = nullptr;
|
||||
}
|
||||
JA_Quit();
|
||||
|
||||
SDL_GL_DestroyContext(glContext);
|
||||
SDL_DestroyWindow(window_);
|
||||
SDL_Quit();
|
||||
|
||||
Reference in New Issue
Block a user