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:
2025-11-16 15:22:06 +01:00
parent 0a269449a3
commit 44de2c7013
27 changed files with 7601 additions and 5 deletions

View File

@@ -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();