Reestructura carpetes: src->source, third_party->source/external, shaders->data/shaders
This commit is contained in:
578
source/audio/jail_audio.cpp
Normal file
578
source/audio/jail_audio.cpp
Normal file
@@ -0,0 +1,578 @@
|
||||
#include "audio/jail_audio.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
// Només declaracions de stb_vorbis: STB_VORBIS_HEADER_ONLY omet el bloc
|
||||
// d'implementació. Les definicions les aporta source/external/stb_vorbis_impl.cpp
|
||||
// (TU aïllat perquè clang-analyzer no dispari fals positius al nostre codi).
|
||||
#define STB_VORBIS_HEADER_ONLY
|
||||
// clang-format off
|
||||
// NOLINTNEXTLINE(bugprone-suspicious-include)
|
||||
#include "stb_vorbis.c"
|
||||
// clang-format on
|
||||
|
||||
namespace Ja {
|
||||
|
||||
// --- Streaming internals (file-scope constants) ---
|
||||
namespace {
|
||||
// Bytes-per-sample per canal (sempre s16)
|
||||
constexpr int MUSIC_BYTES_PER_SAMPLE = 2;
|
||||
// Quants shorts decodifiquem per crida a get_samples_short_interleaved.
|
||||
// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
|
||||
constexpr int MUSIC_CHUNK_SHORTS = 8192;
|
||||
// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
|
||||
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
|
||||
constexpr float MUSIC_LOW_WATER_SECONDS = 0.5F;
|
||||
} // namespace
|
||||
|
||||
// --- Engine::active_ storage ---
|
||||
Engine* Engine::active_ = nullptr;
|
||||
|
||||
auto Engine::active() noexcept -> Engine* { return active_; }
|
||||
|
||||
// --- Ctor/Dtor ---
|
||||
|
||||
Engine::Engine(const int freq, const SDL_AudioFormat format, const int num_channels) {
|
||||
assert(active_ == nullptr && "Ja::Engine: més d'una instància activa no està suportat");
|
||||
active_ = this;
|
||||
|
||||
audio_spec_ = {.format = format, .channels = num_channels, .freq = freq};
|
||||
sdl_audio_device_ = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec_);
|
||||
if (sdl_audio_device_ == 0) { std::fprintf(stderr, "Ja::Engine: Failed to initialize SDL audio!\n"); }
|
||||
for (auto& channel : channels_) { channel.state = ChannelState::FREE; }
|
||||
}
|
||||
|
||||
Engine::~Engine() {
|
||||
if (outgoing_music_.stream != nullptr) {
|
||||
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||
outgoing_music_.stream = nullptr;
|
||||
}
|
||||
if (sdl_audio_device_ != 0) { SDL_CloseAudioDevice(sdl_audio_device_); }
|
||||
sdl_audio_device_ = 0;
|
||||
|
||||
if (active_ == this) { active_ = nullptr; }
|
||||
}
|
||||
|
||||
// --- Helpers stateless (no toquen membres d'Engine) ---
|
||||
namespace {
|
||||
|
||||
auto feedMusicChunk(Music* music) -> int {
|
||||
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return 0; }
|
||||
|
||||
short chunk[MUSIC_CHUNK_SHORTS];
|
||||
const int NUM_CHANNELS = music->spec.channels;
|
||||
const int SAMPLES_PER_CHANNEL = stb_vorbis_get_samples_short_interleaved(
|
||||
music->vorbis,
|
||||
NUM_CHANNELS,
|
||||
static_cast<short*>(chunk),
|
||||
MUSIC_CHUNK_SHORTS);
|
||||
if (SAMPLES_PER_CHANNEL <= 0) { return 0; }
|
||||
|
||||
const int BYTES = SAMPLES_PER_CHANNEL * NUM_CHANNELS * MUSIC_BYTES_PER_SAMPLE;
|
||||
SDL_PutAudioStreamData(music->stream, static_cast<const void*>(chunk), BYTES);
|
||||
return SAMPLES_PER_CHANNEL;
|
||||
}
|
||||
|
||||
void pumpMusic(Music* music) {
|
||||
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; }
|
||||
|
||||
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||
const int LOW_WATER_BYTES = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
|
||||
|
||||
while (SDL_GetAudioStreamAvailable(music->stream) < LOW_WATER_BYTES) {
|
||||
const int DECODED = feedMusicChunk(music);
|
||||
if (DECODED > 0) { continue; }
|
||||
|
||||
// EOF: si queden loops, rebobinar; si no, tallar i deixar drenar.
|
||||
if (music->times != 0) {
|
||||
stb_vorbis_seek_start(music->vorbis);
|
||||
if (music->times > 0) { music->times--; }
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void preFillOutgoing(Music* music, const int duration_ms) {
|
||||
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; }
|
||||
|
||||
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||
const int NEEDED_BYTES = static_cast<int>((static_cast<std::int64_t>(duration_ms) * BYTES_PER_SECOND) / 1000);
|
||||
|
||||
while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) {
|
||||
const int DECODED = feedMusicChunk(music);
|
||||
if (DECODED <= 0) { break; }
|
||||
}
|
||||
}
|
||||
|
||||
// Retorna el progrés lineal [0..1] d'un fade. 1.0 vol dir completat. Única
|
||||
// font de la corba del fade: si es vol canviar a logarítmica/quadràtica,
|
||||
// s'edita aquí i afecta fade-in i fade-out alhora.
|
||||
auto fadeProgress(const FadeState& fade) -> float {
|
||||
if (fade.duration_ms <= 0) { return 1.0F; }
|
||||
const Uint64 ELAPSED = SDL_GetTicks() - fade.start_time;
|
||||
if (ELAPSED >= static_cast<Uint64>(fade.duration_ms)) { return 1.0F; }
|
||||
return static_cast<float>(ELAPSED) / static_cast<float>(fade.duration_ms);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Engine::updateOutgoingFade() {
|
||||
if (outgoing_music_.stream == nullptr || !outgoing_music_.fade.active) { return; }
|
||||
|
||||
const float PROGRESS = fadeProgress(outgoing_music_.fade);
|
||||
if (PROGRESS >= 1.0F) {
|
||||
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||
outgoing_music_.stream = nullptr;
|
||||
outgoing_music_.fade.active = false;
|
||||
} else {
|
||||
SDL_SetAudioStreamGain(outgoing_music_.stream, outgoing_music_.fade.initial_volume * (1.0F - PROGRESS));
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::updateIncomingFade() {
|
||||
if (!incoming_fade_.active) { return; }
|
||||
|
||||
const float PROGRESS = fadeProgress(incoming_fade_);
|
||||
if (PROGRESS >= 1.0F) {
|
||||
incoming_fade_.active = false;
|
||||
SDL_SetAudioStreamGain(current_music_->stream, music_volume_);
|
||||
} else {
|
||||
SDL_SetAudioStreamGain(current_music_->stream, music_volume_ * PROGRESS);
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::updateCurrentMusic() {
|
||||
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
|
||||
|
||||
updateIncomingFade();
|
||||
|
||||
pumpMusic(current_music_);
|
||||
if (current_music_->times == 0 && SDL_GetAudioStreamAvailable(current_music_->stream) == 0) {
|
||||
// La pista ha acabat de drenar naturalment. L'aturem primer (deixa
|
||||
// l'engine en estat consistent) i llavors invoquem el callback;
|
||||
// així un eventual playMusic des del callback comença net.
|
||||
stopMusic();
|
||||
if (on_music_ended_) { on_music_ended_(); }
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::updateSoundChannels() {
|
||||
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
|
||||
if (channels_[i].state != ChannelState::PLAYING) { continue; }
|
||||
|
||||
if (channels_[i].times != 0) {
|
||||
if (static_cast<Uint32>(SDL_GetAudioStreamAvailable(channels_[i].stream)) < (channels_[i].sound->length / 2)) {
|
||||
SDL_PutAudioStreamData(channels_[i].stream, channels_[i].sound->buffer.get(), channels_[i].sound->length);
|
||||
if (channels_[i].times > 0) { channels_[i].times--; }
|
||||
}
|
||||
} else if (SDL_GetAudioStreamAvailable(channels_[i].stream) == 0) {
|
||||
stopChannel(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::stealCurrentIntoOutgoing(const int duration_ms) {
|
||||
if (outgoing_music_.stream != nullptr) {
|
||||
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||
outgoing_music_.stream = nullptr;
|
||||
outgoing_music_.fade.active = false;
|
||||
}
|
||||
|
||||
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING || current_music_->stream == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
preFillOutgoing(current_music_, duration_ms);
|
||||
outgoing_music_.stream = current_music_->stream;
|
||||
outgoing_music_.fade = {
|
||||
.active = true,
|
||||
.start_time = SDL_GetTicks(),
|
||||
.duration_ms = duration_ms,
|
||||
.initial_volume = music_volume_,
|
||||
};
|
||||
current_music_->stream = nullptr;
|
||||
current_music_->state = MusicState::STOPPED;
|
||||
if (current_music_->vorbis != nullptr) { stb_vorbis_seek_start(current_music_->vorbis); }
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
void Engine::forEachTargetChannel(const int channel, Fn&& fn) {
|
||||
if (channel == -1) {
|
||||
for (auto& ch : channels_) { fn(ch); }
|
||||
} else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
|
||||
fn(channels_[channel]);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Engine public API ---
|
||||
|
||||
void Engine::update() {
|
||||
updateOutgoingFade();
|
||||
updateCurrentMusic();
|
||||
updateSoundChannels();
|
||||
}
|
||||
|
||||
void Engine::playMusic(Music* music, const int loop) {
|
||||
if (music == nullptr || music->vorbis == nullptr) { return; }
|
||||
|
||||
stopMusic();
|
||||
|
||||
current_music_ = music;
|
||||
current_music_->state = MusicState::PLAYING;
|
||||
current_music_->times = loop;
|
||||
|
||||
stb_vorbis_seek_start(current_music_->vorbis);
|
||||
|
||||
current_music_->stream = SDL_CreateAudioStream(¤t_music_->spec, &audio_spec_);
|
||||
if (current_music_->stream == nullptr) {
|
||||
std::fprintf(stderr, "Ja::Engine::playMusic: Failed to create audio stream!\n");
|
||||
current_music_->state = MusicState::STOPPED;
|
||||
return;
|
||||
}
|
||||
SDL_SetAudioStreamGain(current_music_->stream, music_volume_);
|
||||
|
||||
pumpMusic(current_music_);
|
||||
|
||||
if (!SDL_BindAudioStream(sdl_audio_device_, current_music_->stream)) {
|
||||
std::fprintf(stderr, "Ja::Engine::playMusic: SDL_BindAudioStream failed!\n");
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::pauseMusic() {
|
||||
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
|
||||
|
||||
current_music_->state = MusicState::PAUSED;
|
||||
SDL_UnbindAudioStream(current_music_->stream);
|
||||
}
|
||||
|
||||
void Engine::resumeMusic() {
|
||||
if (current_music_ == nullptr || current_music_->state != MusicState::PAUSED) { return; }
|
||||
|
||||
current_music_->state = MusicState::PLAYING;
|
||||
SDL_BindAudioStream(sdl_audio_device_, current_music_->stream);
|
||||
}
|
||||
|
||||
void Engine::stopMusic() {
|
||||
if (outgoing_music_.stream != nullptr) {
|
||||
SDL_DestroyAudioStream(outgoing_music_.stream);
|
||||
outgoing_music_.stream = nullptr;
|
||||
outgoing_music_.fade.active = false;
|
||||
}
|
||||
incoming_fade_.active = false;
|
||||
|
||||
if (current_music_ == nullptr || current_music_->state == MusicState::INVALID || current_music_->state == MusicState::STOPPED) { return; }
|
||||
|
||||
current_music_->state = MusicState::STOPPED;
|
||||
if (current_music_->stream != nullptr) {
|
||||
SDL_DestroyAudioStream(current_music_->stream);
|
||||
current_music_->stream = nullptr;
|
||||
}
|
||||
if (current_music_->vorbis != nullptr) {
|
||||
stb_vorbis_seek_start(current_music_->vorbis);
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::fadeOutMusic(const int milliseconds) {
|
||||
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
|
||||
|
||||
stealCurrentIntoOutgoing(milliseconds);
|
||||
incoming_fade_.active = false;
|
||||
}
|
||||
|
||||
void Engine::crossfadeMusic(Music* music, const int crossfade_ms, const int loop) {
|
||||
if (music == nullptr || music->vorbis == nullptr) { return; }
|
||||
|
||||
stealCurrentIntoOutgoing(crossfade_ms);
|
||||
|
||||
current_music_ = music;
|
||||
current_music_->state = MusicState::PLAYING;
|
||||
current_music_->times = loop;
|
||||
|
||||
stb_vorbis_seek_start(current_music_->vorbis);
|
||||
current_music_->stream = SDL_CreateAudioStream(¤t_music_->spec, &audio_spec_);
|
||||
if (current_music_->stream == nullptr) {
|
||||
std::fprintf(stderr, "Ja::Engine::crossfadeMusic: Failed to create audio stream!\n");
|
||||
current_music_->state = MusicState::STOPPED;
|
||||
return;
|
||||
}
|
||||
SDL_SetAudioStreamGain(current_music_->stream, 0.0F);
|
||||
pumpMusic(current_music_);
|
||||
SDL_BindAudioStream(sdl_audio_device_, current_music_->stream);
|
||||
|
||||
incoming_fade_ = {
|
||||
.active = true,
|
||||
.start_time = SDL_GetTicks(),
|
||||
.duration_ms = crossfade_ms,
|
||||
.initial_volume = 0.0F,
|
||||
};
|
||||
}
|
||||
|
||||
auto Engine::getMusicState() const -> MusicState {
|
||||
if (current_music_ == nullptr) { return MusicState::INVALID; }
|
||||
return current_music_->state;
|
||||
}
|
||||
|
||||
auto Engine::setMusicVolume(float volume) -> float {
|
||||
music_volume_ = SDL_clamp(volume, 0.0F, 1.0F);
|
||||
if (current_music_ != nullptr && current_music_->stream != nullptr) {
|
||||
SDL_SetAudioStreamGain(current_music_->stream, music_volume_);
|
||||
}
|
||||
return music_volume_;
|
||||
}
|
||||
|
||||
void Engine::setOnMusicEnded(std::function<void()> callback) {
|
||||
on_music_ended_ = std::move(callback);
|
||||
}
|
||||
|
||||
void Engine::onMusicDeleted(const Music* music) {
|
||||
if (music == nullptr) { return; }
|
||||
if (current_music_ == music) {
|
||||
stopMusic();
|
||||
current_music_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sound ---
|
||||
|
||||
auto Engine::playSound(Sound* sound, const int loop, const int group) -> int {
|
||||
if (sound == nullptr) { return -1; }
|
||||
|
||||
int channel = 0;
|
||||
while (channel < MAX_SIMULTANEOUS_CHANNELS && channels_[channel].state != ChannelState::FREE) { channel++; }
|
||||
if (channel == MAX_SIMULTANEOUS_CHANNELS) {
|
||||
// No hay canal libre, reemplazamos el primero
|
||||
channel = 0;
|
||||
}
|
||||
|
||||
return playSoundOnChannel(sound, channel, loop, group);
|
||||
}
|
||||
|
||||
auto Engine::playSoundOnChannel(Sound* sound, const int channel, const int loop, const int group) -> int {
|
||||
if (sound == nullptr) { return -1; }
|
||||
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return -1; }
|
||||
|
||||
stopChannel(channel);
|
||||
|
||||
channels_[channel].sound = sound;
|
||||
channels_[channel].times = loop;
|
||||
channels_[channel].pos = 0;
|
||||
channels_[channel].group = group;
|
||||
channels_[channel].state = ChannelState::PLAYING;
|
||||
channels_[channel].stream = SDL_CreateAudioStream(&channels_[channel].sound->spec, &audio_spec_);
|
||||
|
||||
if (channels_[channel].stream == nullptr) {
|
||||
std::fprintf(stderr, "Ja::Engine::playSoundOnChannel: Failed to create audio stream!\n");
|
||||
channels_[channel].state = ChannelState::FREE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
SDL_PutAudioStreamData(channels_[channel].stream, channels_[channel].sound->buffer.get(), channels_[channel].sound->length);
|
||||
SDL_SetAudioStreamGain(channels_[channel].stream, sound_volume_[group]);
|
||||
SDL_BindAudioStream(sdl_audio_device_, channels_[channel].stream);
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
void Engine::pauseChannel(const int channel) {
|
||||
forEachTargetChannel(channel, [](Channel& ch) {
|
||||
if (ch.state == ChannelState::PLAYING) {
|
||||
ch.state = ChannelState::PAUSED;
|
||||
SDL_UnbindAudioStream(ch.stream);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Engine::resumeChannel(const int channel) {
|
||||
const SDL_AudioDeviceID DEVICE = sdl_audio_device_;
|
||||
forEachTargetChannel(channel, [DEVICE](Channel& ch) {
|
||||
if (ch.state == ChannelState::PAUSED) {
|
||||
ch.state = ChannelState::PLAYING;
|
||||
SDL_BindAudioStream(DEVICE, ch.stream);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Engine::stopChannel(const int channel) {
|
||||
forEachTargetChannel(channel, [this](Channel& ch) {
|
||||
if (ch.state != ChannelState::FREE) {
|
||||
if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); }
|
||||
ch.stream = nullptr;
|
||||
ch.state = ChannelState::FREE;
|
||||
ch.pos = 0;
|
||||
ch.sound = nullptr;
|
||||
if (ch.has_effect) {
|
||||
ch.has_effect = false;
|
||||
if (effect_channels_active_ > 0) { --effect_channels_active_; }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
auto Engine::setSoundVolume(float volume, const int group) -> float {
|
||||
const float V = SDL_clamp(volume, 0.0F, 1.0F);
|
||||
|
||||
if (group == -1) {
|
||||
std::fill(std::begin(sound_volume_), std::end(sound_volume_), V);
|
||||
} else if (group >= 0 && group < MAX_GROUPS) {
|
||||
sound_volume_[group] = V;
|
||||
} else {
|
||||
return V;
|
||||
}
|
||||
|
||||
for (auto& ch : channels_) {
|
||||
if ((ch.state == ChannelState::PLAYING) || (ch.state == ChannelState::PAUSED)) {
|
||||
if (group == -1 || ch.group == group) {
|
||||
if (ch.stream != nullptr) {
|
||||
SDL_SetAudioStreamGain(ch.stream, sound_volume_[ch.group]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return V;
|
||||
}
|
||||
|
||||
void Engine::onSoundDeleted(const Sound* sound) {
|
||||
if (sound == nullptr) { return; }
|
||||
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
|
||||
if (channels_[i].sound == sound) { stopChannel(i); }
|
||||
}
|
||||
}
|
||||
|
||||
auto Engine::playProcessedOnFreeChannel(const std::vector<std::uint8_t>& bytes, const SDL_AudioSpec& spec, const int group) -> int {
|
||||
// El cap de canals amb efecte es valida abans de reservar slot —
|
||||
// així evitem crear i destruir un stream només per descartar el play.
|
||||
if (effect_channels_active_ >= MAX_EFFECT_CHANNELS) { return -1; }
|
||||
|
||||
int channel = 0;
|
||||
while (channel < MAX_SIMULTANEOUS_CHANNELS && channels_[channel].state != ChannelState::FREE) { ++channel; }
|
||||
if (channel == MAX_SIMULTANEOUS_CHANNELS) { channel = 0; }
|
||||
|
||||
stopChannel(channel);
|
||||
|
||||
// El stream es crea contra l'spec del buffer processat (S16, ...)
|
||||
// perquè SDL faci el resampling cap a audio_spec_ del device.
|
||||
channels_[channel].stream = SDL_CreateAudioStream(&spec, &audio_spec_);
|
||||
if (channels_[channel].stream == nullptr) {
|
||||
std::fprintf(stderr, "Ja::Engine::playProcessedOnFreeChannel: Failed to create audio stream!\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
channels_[channel].sound = nullptr; // El buffer no és propietat de cap Ja::Sound.
|
||||
channels_[channel].times = 0;
|
||||
channels_[channel].pos = 0;
|
||||
const int CLAMPED_GROUP = (group >= 0 && group < MAX_GROUPS) ? group : 0;
|
||||
channels_[channel].group = CLAMPED_GROUP;
|
||||
channels_[channel].state = ChannelState::PLAYING;
|
||||
channels_[channel].has_effect = true;
|
||||
++effect_channels_active_;
|
||||
|
||||
SDL_PutAudioStreamData(channels_[channel].stream, bytes.data(), static_cast<int>(bytes.size()));
|
||||
SDL_SetAudioStreamGain(channels_[channel].stream, sound_volume_[CLAMPED_GROUP]);
|
||||
SDL_BindAudioStream(sdl_audio_device_, channels_[channel].stream);
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
// shadertoy build: AudioEffects is not bundled. Both echo/reverb entry
|
||||
// points are stubbed; if an effect is requested, fall back to playing
|
||||
// the dry sound so callers don't lose audio.
|
||||
auto Engine::playSoundWithEcho(const Sound* sound, const EchoParams& /*params*/, const int group) -> int {
|
||||
if (sound == nullptr) { return -1; }
|
||||
return playSound(const_cast<Sound*>(sound), 0, group);
|
||||
}
|
||||
|
||||
auto Engine::playSoundWithReverb(const Sound* sound, const ReverbParams& /*params*/, const int group) -> int {
|
||||
if (sound == nullptr) { return -1; }
|
||||
return playSound(const_cast<Sound*>(sound), 0, group);
|
||||
}
|
||||
|
||||
// --- Factories i destructors (permanents) ---
|
||||
|
||||
auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
|
||||
if (buffer == nullptr || length == 0) { return nullptr; }
|
||||
|
||||
// Allocem el Music primer per aprofitar el seu `std::vector<Uint8>`
|
||||
// com a propietari del OGG comprimit. stb_vorbis guarda un punter
|
||||
// persistent al buffer; com que ací no el resize'jem, el .data() és
|
||||
// estable durant tot el cicle de vida del music.
|
||||
auto music = std::make_unique<Music>();
|
||||
music->ogg_data.assign(buffer, buffer + length);
|
||||
|
||||
int vorbis_error = 0;
|
||||
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
|
||||
static_cast<int>(length),
|
||||
&vorbis_error,
|
||||
nullptr);
|
||||
if (music->vorbis == nullptr) {
|
||||
std::fprintf(stderr, "Ja::loadMusic: stb_vorbis_open_memory failed (error %d)\n", vorbis_error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const stb_vorbis_info INFO = stb_vorbis_get_info(music->vorbis);
|
||||
music->spec.channels = static_cast<int>(INFO.channels);
|
||||
music->spec.freq = static_cast<int>(INFO.sample_rate);
|
||||
music->spec.format = SDL_AUDIO_S16;
|
||||
music->state = MusicState::STOPPED;
|
||||
|
||||
return music.release();
|
||||
}
|
||||
|
||||
// Overload amb filename. Resource::Cache l'usa per registrar el path dins
|
||||
// del propi Ja::Music (camp `filename`); la capa Audio l'usa per recuperar
|
||||
// el nom després d'un playMusic(Ja::Music*, ...) — veure PATCH-02.
|
||||
auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music* {
|
||||
Music* music = loadMusic(buffer, length);
|
||||
if (music != nullptr && filename != nullptr) { music->filename = filename; }
|
||||
return music;
|
||||
}
|
||||
|
||||
void deleteMusic(Music* music) {
|
||||
if (music == nullptr) { return; }
|
||||
// Notifiquem el motor actiu perquè pari la pista si és la current_music.
|
||||
// Si no hi ha motor (shutdown-order invertit), passem: els recursos
|
||||
// propis del Music es lliberen igualment a sota.
|
||||
if (Engine* eng = Engine::active()) { eng->onMusicDeleted(music); }
|
||||
|
||||
if (music->stream != nullptr) { SDL_DestroyAudioStream(music->stream); }
|
||||
if (music->vorbis != nullptr) { stb_vorbis_close(music->vorbis); }
|
||||
delete music;
|
||||
}
|
||||
|
||||
auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* {
|
||||
auto sound = std::make_unique<Sound>();
|
||||
Uint8* raw = nullptr;
|
||||
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), true, &sound->spec, &raw, &sound->length)) {
|
||||
std::fprintf(stderr, "Ja::loadSound: Failed to load WAV from memory: %s\n", SDL_GetError());
|
||||
return nullptr;
|
||||
}
|
||||
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
|
||||
return sound.release();
|
||||
}
|
||||
|
||||
void deleteSound(Sound* sound) {
|
||||
if (sound == nullptr) { return; }
|
||||
if (Engine* eng = Engine::active()) { eng->onSoundDeleted(sound); }
|
||||
// buffer es destrueix automàticament via RAII (SdlFreeDeleter).
|
||||
delete sound;
|
||||
}
|
||||
|
||||
} // namespace Ja
|
||||
|
||||
// --- stb_vorbis macro leak cleanup ---
|
||||
// stb_vorbis.c filtra noms curts (L, C, R i PLAYBACK_*) al TU que el compila.
|
||||
// Xocarien amb paràmetres de plantilla d'altres headers si aquestes definicions
|
||||
// s'escapessin. Els netegem al final del TU per tancar la porta.
|
||||
// clang-format off
|
||||
#undef L
|
||||
#undef C
|
||||
#undef R
|
||||
#undef PLAYBACK_MONO
|
||||
#undef PLAYBACK_LEFT
|
||||
#undef PLAYBACK_RIGHT
|
||||
// clang-format on
|
||||
229
source/audio/jail_audio.hpp
Normal file
229
source/audio/jail_audio.hpp
Normal file
@@ -0,0 +1,229 @@
|
||||
#pragma once
|
||||
|
||||
// --- Includes ---
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Forward-declaració del decoder de vorbis. La implementació viu a
|
||||
// jail_audio.cpp (únic TU que compila external/stb_vorbis.c). Qualsevol caller
|
||||
// només necessita `stb_vorbis*` per punter — mai per valor — així que el
|
||||
// forward decl n'hi ha prou i evita arrossegar el .c a tots els TU.
|
||||
// NOLINTNEXTLINE(readability-identifier-naming) — nom imposat per l'API de stb_vorbis
|
||||
struct stb_vorbis;
|
||||
|
||||
// Deleter stateless per a buffers reservats amb `SDL_malloc` / `SDL_LoadWAV*`.
|
||||
// Compatible amb `std::unique_ptr<Uint8[], SdlFreeDeleter>` — zero size overhead
|
||||
// gràcies a EBO, igual que un unique_ptr amb default_delete.
|
||||
struct SdlFreeDeleter {
|
||||
void operator()(Uint8* p) const noexcept {
|
||||
if (p != nullptr) { SDL_free(p); }
|
||||
}
|
||||
};
|
||||
|
||||
// Motor de baix nivell d'àudio del projecte jailgames: streaming OGG
|
||||
// (stb_vorbis) + N canals d'efectes (SDL3 audio). No depèn d'Options ni de cap
|
||||
// singleton del joc; només de SDL3 i stb_vorbis. La capa superior (Audio) li
|
||||
// passa recursos pel punter i fa el bookkeeping d'usuari.
|
||||
namespace Ja {
|
||||
|
||||
// --- Public Enums ---
|
||||
enum class ChannelState : std::uint8_t {
|
||||
FREE,
|
||||
PLAYING,
|
||||
PAUSED,
|
||||
};
|
||||
|
||||
enum class MusicState : std::uint8_t {
|
||||
INVALID, // Music carregat però mai play-ejat
|
||||
PLAYING,
|
||||
PAUSED,
|
||||
STOPPED,
|
||||
};
|
||||
|
||||
// --- Constants ---
|
||||
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20;
|
||||
inline constexpr int MAX_GROUPS = 2;
|
||||
// Cap superior de canals que poden estar simultàniament reproduint un so
|
||||
// amb efecte (eco/reverb). Si està al límit, les noves crides amb efecte
|
||||
// cauen al camí sec — l'usuari sent el so igualment, sense la cua.
|
||||
inline constexpr int MAX_EFFECT_CHANNELS = 4;
|
||||
|
||||
// --- Paràmetres d'efectes ---
|
||||
// Els camps els fixa el caller (Audio) llegint sounds.yaml; el motor només
|
||||
// els passa a AudioEffects::applyEcho/applyReverb. Els defaults són
|
||||
// sensats però els presets els sobreescriuen.
|
||||
struct EchoParams {
|
||||
float delay_ms{220.0F}; // Temps fins al primer rebot.
|
||||
float feedback{0.45F}; // Reinjecció (0..0.95).
|
||||
float wet{0.35F}; // Mescla humida (0..1).
|
||||
};
|
||||
|
||||
struct ReverbParams {
|
||||
float room_size{0.7F}; // Mida percebuda (0..1).
|
||||
float damping{0.5F}; // Atenuació d'aguts per rebot (0..1).
|
||||
float wet{0.4F}; // Mescla humida (0..1).
|
||||
};
|
||||
|
||||
// Spec de fallback del dispositiu. S'aplica abans que l'Engine s'iniciï i
|
||||
// com a valor inicial de Sound/Music. L'spec real d'ús l'imposa el ctor
|
||||
// d'Engine, alimentat des de Defaults::Audio via Audio.
|
||||
inline constexpr SDL_AudioSpec DEFAULT_SPEC{SDL_AUDIO_S16, 2, 48000};
|
||||
|
||||
// --- Struct Definitions ---
|
||||
struct Sound {
|
||||
SDL_AudioSpec spec{DEFAULT_SPEC};
|
||||
Uint32 length{0};
|
||||
// Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV
|
||||
// via SDL_malloc; el deleter `SdlFreeDeleter` allibera amb SDL_free.
|
||||
std::unique_ptr<Uint8[], SdlFreeDeleter> buffer;
|
||||
};
|
||||
|
||||
// L'ordre (punters primer, ints després, enum de 8 bits al final) minimitza
|
||||
// el padding a 64-bit (evita avisos de clang-analyzer-optin.performance.Padding).
|
||||
struct Channel {
|
||||
Sound* sound{nullptr};
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
int pos{0};
|
||||
int times{0};
|
||||
int group{0};
|
||||
ChannelState state{ChannelState::FREE};
|
||||
// Marca si aquest canal va arrencar amb so processat per un efecte.
|
||||
// El motor compta canals actius amb efecte per fer complir
|
||||
// MAX_EFFECT_CHANNELS i alliberar el comptador en parar.
|
||||
bool has_effect{false};
|
||||
};
|
||||
|
||||
struct Music {
|
||||
SDL_AudioSpec spec{DEFAULT_SPEC};
|
||||
|
||||
// OGG comprimit en memòria. Propietat nostra; es copia des del buffer
|
||||
// d'entrada una sola vegada en loadMusic i es descomprimix en chunks
|
||||
// per streaming. Com que stb_vorbis guarda un punter persistent al
|
||||
// `.data()` d'aquest vector, no el podem resize'jar un cop establert
|
||||
// (una reallocation invalidaria el punter que el decoder conserva).
|
||||
std::vector<Uint8> ogg_data;
|
||||
stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del Music
|
||||
|
||||
std::string filename;
|
||||
|
||||
int times{0}; // loops restants (-1 = infinit, 0 = un sol play)
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
MusicState state{MusicState::INVALID};
|
||||
};
|
||||
|
||||
struct FadeState {
|
||||
bool active{false};
|
||||
Uint64 start_time{0};
|
||||
int duration_ms{0};
|
||||
float initial_volume{0.0F};
|
||||
};
|
||||
|
||||
struct OutgoingMusic {
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
FadeState fade;
|
||||
};
|
||||
|
||||
// --- Engine ---
|
||||
// Encapsula tot l'estat que abans vivia com a globals inline. Un sol Engine
|
||||
// viu per procés (enforceat via assert al ctor contra `active_`). El ctor
|
||||
// obre el device SDL; el dtor el tanca (RAII). Els deleters
|
||||
// `Ja::deleteMusic`/`Ja::deleteSound` accedeixen al motor actiu via
|
||||
// `Engine::active()` per parar canals abans d'alliberar.
|
||||
class Engine {
|
||||
public:
|
||||
Engine(int freq, SDL_AudioFormat format, int num_channels);
|
||||
~Engine();
|
||||
Engine(const Engine&) = delete;
|
||||
auto operator=(const Engine&) -> Engine& = delete;
|
||||
Engine(Engine&&) = delete;
|
||||
auto operator=(Engine&&) -> Engine& = delete;
|
||||
|
||||
// Retorna el motor actiu o nullptr si cap ha estat construït. L'usen
|
||||
// els deleters de recursos perquè no els arriba cap referència directa.
|
||||
[[nodiscard]] static auto active() noexcept -> Engine*;
|
||||
|
||||
void update();
|
||||
|
||||
// --- Música ---
|
||||
void playMusic(Music* music, int loop = -1);
|
||||
void pauseMusic();
|
||||
void resumeMusic();
|
||||
void stopMusic();
|
||||
void fadeOutMusic(int milliseconds);
|
||||
void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
|
||||
[[nodiscard]] auto getMusicState() const -> MusicState;
|
||||
auto setMusicVolume(float volume) -> float;
|
||||
// Registra un callback que es disparà quan la música actual acabi de
|
||||
// drenar naturalment (times == 0 + stream buit). Es crida DESPRÉS de
|
||||
// stopMusic, així que el callback pot invocar playMusic sense córrer.
|
||||
// S'executa al mateix thread que Engine::update (render loop); no fer
|
||||
// operacions blocants.
|
||||
void setOnMusicEnded(std::function<void()> callback);
|
||||
// Notifica al motor que un Music s'està destruint: si és el current_music
|
||||
// s'atura abans que els seus recursos (stream/vorbis) deixin de ser vàlids.
|
||||
void onMusicDeleted(const Music* music);
|
||||
|
||||
// --- So ---
|
||||
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
|
||||
auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
|
||||
// Reproducció amb so processat per un efecte. Retorna el canal
|
||||
// assignat o -1 si no queden slots d'efecte (MAX_EFFECT_CHANNELS).
|
||||
// El sound original només s'usa per consultar el spec/buffer; el
|
||||
// canal manipula el buffer ja processat (no reapunta a `sound`).
|
||||
auto playSoundWithEcho(const Sound* sound, const EchoParams& params, int group = 0) -> int;
|
||||
auto playSoundWithReverb(const Sound* sound, const ReverbParams& params, int group = 0) -> int;
|
||||
void pauseChannel(int channel);
|
||||
void resumeChannel(int channel);
|
||||
void stopChannel(int channel);
|
||||
auto setSoundVolume(float volume, int group = -1) -> float;
|
||||
// Notifica al motor que un Sound s'està destruint: els canals que el
|
||||
// referenciïn es paren abans d'alliberar el buffer.
|
||||
void onSoundDeleted(const Sound* sound);
|
||||
|
||||
private:
|
||||
void stealCurrentIntoOutgoing(int duration_ms);
|
||||
void updateOutgoingFade();
|
||||
void updateIncomingFade();
|
||||
void updateCurrentMusic();
|
||||
void updateSoundChannels();
|
||||
// Empenta un buffer ja processat (S16) a un canal lliure i el deixa
|
||||
// sonar sense bucle. Camí comú dels dos overloads playSoundWith*.
|
||||
// Retorna el canal o -1 si no queden slots.
|
||||
auto playProcessedOnFreeChannel(const std::vector<std::uint8_t>& bytes, const SDL_AudioSpec& spec, int group) -> int;
|
||||
|
||||
template <typename Fn>
|
||||
void forEachTargetChannel(int channel, Fn&& fn);
|
||||
|
||||
Music* current_music_{nullptr};
|
||||
Channel channels_[MAX_SIMULTANEOUS_CHANNELS]{};
|
||||
SDL_AudioSpec audio_spec_{DEFAULT_SPEC};
|
||||
float music_volume_{1.0F};
|
||||
float sound_volume_[MAX_GROUPS]{};
|
||||
SDL_AudioDeviceID sdl_audio_device_{0};
|
||||
OutgoingMusic outgoing_music_;
|
||||
FadeState incoming_fade_;
|
||||
std::function<void()> on_music_ended_;
|
||||
// Comptador derivat de Channel::has_effect — evita haver-lo de
|
||||
// recalcular cada vegada que algú demana un play amb efecte.
|
||||
int effect_channels_active_{0};
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming) — convenció projecte: private static amb sufix _
|
||||
static Engine* active_;
|
||||
};
|
||||
|
||||
// --- Factories i destructors (permanents) ---
|
||||
// No depenen de l'estat del motor: loadMusic/loadSound només construeixen
|
||||
// objectes, deleteMusic/deleteSound consulten Engine::active() per parar
|
||||
// canals abans d'alliberar (si el motor encara viu).
|
||||
[[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length) -> Music*;
|
||||
[[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music*;
|
||||
void deleteMusic(Music* music);
|
||||
[[nodiscard]] auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound*;
|
||||
void deleteSound(Sound* sound);
|
||||
|
||||
} // namespace Ja
|
||||
67
source/defines.hpp
Normal file
67
source/defines.hpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
// Nombre de la aplicación
|
||||
constexpr const char* APP_NAME = "Shadertoy";
|
||||
|
||||
// Prefijo del título de la ventana (estilo aee_2026).
|
||||
constexpr const char* WINDOW_TITLE_PREFIX = "\xC2\xA9 2025 Shadertoy \xE2\x80\x94 JailDesigner";
|
||||
|
||||
// Tamaño de ventana por defecto
|
||||
constexpr int WINDOW_WIDTH = 800;
|
||||
constexpr int WINDOW_HEIGHT = 800;
|
||||
|
||||
// Includes específicos por plataforma para obtener la ruta del ejecutable
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <limits.h>
|
||||
#include <mach-o/dyld.h>
|
||||
#else
|
||||
#include <limits.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
// Función auxiliar para obtener la ruta del directorio del ejecutable
|
||||
inline std::string getExecutableDirectory() {
|
||||
#ifdef _WIN32
|
||||
char buffer[MAX_PATH];
|
||||
GetModuleFileNameA(NULL, buffer, MAX_PATH);
|
||||
std::filesystem::path exe_path(buffer);
|
||||
return exe_path.parent_path().string();
|
||||
#elif defined(__APPLE__)
|
||||
char buffer[PATH_MAX];
|
||||
uint32_t size = sizeof(buffer);
|
||||
if (_NSGetExecutablePath(buffer, &size) == 0) {
|
||||
std::filesystem::path exe_path(buffer);
|
||||
return exe_path.parent_path().string();
|
||||
}
|
||||
return ".";
|
||||
#else
|
||||
// Linux y otros Unix
|
||||
char buffer[PATH_MAX];
|
||||
ssize_t len = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1);
|
||||
if (len != -1) {
|
||||
buffer[len] = '\0';
|
||||
std::filesystem::path exe_path(buffer);
|
||||
return exe_path.parent_path().string();
|
||||
}
|
||||
return ".";
|
||||
#endif
|
||||
}
|
||||
|
||||
// Función auxiliar para obtener la ruta del directorio de recursos
|
||||
inline std::string getResourcesDirectory() {
|
||||
std::string exe_dir = getExecutableDirectory();
|
||||
|
||||
#ifdef MACOS_BUNDLE
|
||||
// En macOS Bundle: ejecutable está en Contents/MacOS/, recursos en Contents/Resources/
|
||||
std::filesystem::path resources_path = std::filesystem::path(exe_dir) / ".." / "Resources";
|
||||
return resources_path.string();
|
||||
#else
|
||||
// En desarrollo o releases normales: recursos están junto al ejecutable
|
||||
return exe_dir;
|
||||
#endif
|
||||
}
|
||||
311
source/external/glad/include/KHR/khrplatform.h
vendored
Normal file
311
source/external/glad/include/KHR/khrplatform.h
vendored
Normal file
@@ -0,0 +1,311 @@
|
||||
#ifndef __khrplatform_h_
|
||||
#define __khrplatform_h_
|
||||
|
||||
/*
|
||||
** Copyright (c) 2008-2018 The Khronos Group Inc.
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person obtaining a
|
||||
** copy of this software and/or associated documentation files (the
|
||||
** "Materials"), to deal in the Materials without restriction, including
|
||||
** without limitation the rights to use, copy, modify, merge, publish,
|
||||
** distribute, sublicense, and/or sell copies of the Materials, and to
|
||||
** permit persons to whom the Materials are furnished to do so, subject to
|
||||
** the following conditions:
|
||||
**
|
||||
** The above copyright notice and this permission notice shall be included
|
||||
** in all copies or substantial portions of the Materials.
|
||||
**
|
||||
** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||
*/
|
||||
|
||||
/* Khronos platform-specific types and definitions.
|
||||
*
|
||||
* The master copy of khrplatform.h is maintained in the Khronos EGL
|
||||
* Registry repository at https://github.com/KhronosGroup/EGL-Registry
|
||||
* The last semantic modification to khrplatform.h was at commit ID:
|
||||
* 67a3e0864c2d75ea5287b9f3d2eb74a745936692
|
||||
*
|
||||
* Adopters may modify this file to suit their platform. Adopters are
|
||||
* encouraged to submit platform specific modifications to the Khronos
|
||||
* group so that they can be included in future versions of this file.
|
||||
* Please submit changes by filing pull requests or issues on
|
||||
* the EGL Registry repository linked above.
|
||||
*
|
||||
*
|
||||
* See the Implementer's Guidelines for information about where this file
|
||||
* should be located on your system and for more details of its use:
|
||||
* http://www.khronos.org/registry/implementers_guide.pdf
|
||||
*
|
||||
* This file should be included as
|
||||
* #include <KHR/khrplatform.h>
|
||||
* by Khronos client API header files that use its types and defines.
|
||||
*
|
||||
* The types in khrplatform.h should only be used to define API-specific types.
|
||||
*
|
||||
* Types defined in khrplatform.h:
|
||||
* khronos_int8_t signed 8 bit
|
||||
* khronos_uint8_t unsigned 8 bit
|
||||
* khronos_int16_t signed 16 bit
|
||||
* khronos_uint16_t unsigned 16 bit
|
||||
* khronos_int32_t signed 32 bit
|
||||
* khronos_uint32_t unsigned 32 bit
|
||||
* khronos_int64_t signed 64 bit
|
||||
* khronos_uint64_t unsigned 64 bit
|
||||
* khronos_intptr_t signed same number of bits as a pointer
|
||||
* khronos_uintptr_t unsigned same number of bits as a pointer
|
||||
* khronos_ssize_t signed size
|
||||
* khronos_usize_t unsigned size
|
||||
* khronos_float_t signed 32 bit floating point
|
||||
* khronos_time_ns_t unsigned 64 bit time in nanoseconds
|
||||
* khronos_utime_nanoseconds_t unsigned time interval or absolute time in
|
||||
* nanoseconds
|
||||
* khronos_stime_nanoseconds_t signed time interval in nanoseconds
|
||||
* khronos_boolean_enum_t enumerated boolean type. This should
|
||||
* only be used as a base type when a client API's boolean type is
|
||||
* an enum. Client APIs which use an integer or other type for
|
||||
* booleans cannot use this as the base type for their boolean.
|
||||
*
|
||||
* Tokens defined in khrplatform.h:
|
||||
*
|
||||
* KHRONOS_FALSE, KHRONOS_TRUE Enumerated boolean false/true values.
|
||||
*
|
||||
* KHRONOS_SUPPORT_INT64 is 1 if 64 bit integers are supported; otherwise 0.
|
||||
* KHRONOS_SUPPORT_FLOAT is 1 if floats are supported; otherwise 0.
|
||||
*
|
||||
* Calling convention macros defined in this file:
|
||||
* KHRONOS_APICALL
|
||||
* KHRONOS_APIENTRY
|
||||
* KHRONOS_APIATTRIBUTES
|
||||
*
|
||||
* These may be used in function prototypes as:
|
||||
*
|
||||
* KHRONOS_APICALL void KHRONOS_APIENTRY funcname(
|
||||
* int arg1,
|
||||
* int arg2) KHRONOS_APIATTRIBUTES;
|
||||
*/
|
||||
|
||||
#if defined(__SCITECH_SNAP__) && !defined(KHRONOS_STATIC)
|
||||
# define KHRONOS_STATIC 1
|
||||
#endif
|
||||
|
||||
/*-------------------------------------------------------------------------
|
||||
* Definition of KHRONOS_APICALL
|
||||
*-------------------------------------------------------------------------
|
||||
* This precedes the return type of the function in the function prototype.
|
||||
*/
|
||||
#if defined(KHRONOS_STATIC)
|
||||
/* If the preprocessor constant KHRONOS_STATIC is defined, make the
|
||||
* header compatible with static linking. */
|
||||
# define KHRONOS_APICALL
|
||||
#elif defined(_WIN32)
|
||||
# define KHRONOS_APICALL __declspec(dllimport)
|
||||
#elif defined (__SYMBIAN32__)
|
||||
# define KHRONOS_APICALL IMPORT_C
|
||||
#elif defined(__ANDROID__)
|
||||
# define KHRONOS_APICALL __attribute__((visibility("default")))
|
||||
#else
|
||||
# define KHRONOS_APICALL
|
||||
#endif
|
||||
|
||||
/*-------------------------------------------------------------------------
|
||||
* Definition of KHRONOS_APIENTRY
|
||||
*-------------------------------------------------------------------------
|
||||
* This follows the return type of the function and precedes the function
|
||||
* name in the function prototype.
|
||||
*/
|
||||
#if defined(_WIN32) && !defined(_WIN32_WCE) && !defined(__SCITECH_SNAP__)
|
||||
/* Win32 but not WinCE */
|
||||
# define KHRONOS_APIENTRY __stdcall
|
||||
#else
|
||||
# define KHRONOS_APIENTRY
|
||||
#endif
|
||||
|
||||
/*-------------------------------------------------------------------------
|
||||
* Definition of KHRONOS_APIATTRIBUTES
|
||||
*-------------------------------------------------------------------------
|
||||
* This follows the closing parenthesis of the function prototype arguments.
|
||||
*/
|
||||
#if defined (__ARMCC_2__)
|
||||
#define KHRONOS_APIATTRIBUTES __softfp
|
||||
#else
|
||||
#define KHRONOS_APIATTRIBUTES
|
||||
#endif
|
||||
|
||||
/*-------------------------------------------------------------------------
|
||||
* basic type definitions
|
||||
*-----------------------------------------------------------------------*/
|
||||
#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__GNUC__) || defined(__SCO__) || defined(__USLC__)
|
||||
|
||||
|
||||
/*
|
||||
* Using <stdint.h>
|
||||
*/
|
||||
#include <stdint.h>
|
||||
typedef int32_t khronos_int32_t;
|
||||
typedef uint32_t khronos_uint32_t;
|
||||
typedef int64_t khronos_int64_t;
|
||||
typedef uint64_t khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
/*
|
||||
* To support platform where unsigned long cannot be used interchangeably with
|
||||
* inptr_t (e.g. CHERI-extended ISAs), we can use the stdint.h intptr_t.
|
||||
* Ideally, we could just use (u)intptr_t everywhere, but this could result in
|
||||
* ABI breakage if khronos_uintptr_t is changed from unsigned long to
|
||||
* unsigned long long or similar (this results in different C++ name mangling).
|
||||
* To avoid changes for existing platforms, we restrict usage of intptr_t to
|
||||
* platforms where the size of a pointer is larger than the size of long.
|
||||
*/
|
||||
#if defined(__SIZEOF_LONG__) && defined(__SIZEOF_POINTER__)
|
||||
#if __SIZEOF_POINTER__ > __SIZEOF_LONG__
|
||||
#define KHRONOS_USE_INTPTR_T
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#elif defined(__VMS ) || defined(__sgi)
|
||||
|
||||
/*
|
||||
* Using <inttypes.h>
|
||||
*/
|
||||
#include <inttypes.h>
|
||||
typedef int32_t khronos_int32_t;
|
||||
typedef uint32_t khronos_uint32_t;
|
||||
typedef int64_t khronos_int64_t;
|
||||
typedef uint64_t khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#elif defined(_WIN32) && !defined(__SCITECH_SNAP__)
|
||||
|
||||
/*
|
||||
* Win32
|
||||
*/
|
||||
typedef __int32 khronos_int32_t;
|
||||
typedef unsigned __int32 khronos_uint32_t;
|
||||
typedef __int64 khronos_int64_t;
|
||||
typedef unsigned __int64 khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#elif defined(__sun__) || defined(__digital__)
|
||||
|
||||
/*
|
||||
* Sun or Digital
|
||||
*/
|
||||
typedef int khronos_int32_t;
|
||||
typedef unsigned int khronos_uint32_t;
|
||||
#if defined(__arch64__) || defined(_LP64)
|
||||
typedef long int khronos_int64_t;
|
||||
typedef unsigned long int khronos_uint64_t;
|
||||
#else
|
||||
typedef long long int khronos_int64_t;
|
||||
typedef unsigned long long int khronos_uint64_t;
|
||||
#endif /* __arch64__ */
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#elif 0
|
||||
|
||||
/*
|
||||
* Hypothetical platform with no float or int64 support
|
||||
*/
|
||||
typedef int khronos_int32_t;
|
||||
typedef unsigned int khronos_uint32_t;
|
||||
#define KHRONOS_SUPPORT_INT64 0
|
||||
#define KHRONOS_SUPPORT_FLOAT 0
|
||||
|
||||
#else
|
||||
|
||||
/*
|
||||
* Generic fallback
|
||||
*/
|
||||
#include <stdint.h>
|
||||
typedef int32_t khronos_int32_t;
|
||||
typedef uint32_t khronos_uint32_t;
|
||||
typedef int64_t khronos_int64_t;
|
||||
typedef uint64_t khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
/*
|
||||
* Types that are (so far) the same on all platforms
|
||||
*/
|
||||
typedef signed char khronos_int8_t;
|
||||
typedef unsigned char khronos_uint8_t;
|
||||
typedef signed short int khronos_int16_t;
|
||||
typedef unsigned short int khronos_uint16_t;
|
||||
|
||||
/*
|
||||
* Types that differ between LLP64 and LP64 architectures - in LLP64,
|
||||
* pointers are 64 bits, but 'long' is still 32 bits. Win64 appears
|
||||
* to be the only LLP64 architecture in current use.
|
||||
*/
|
||||
#ifdef KHRONOS_USE_INTPTR_T
|
||||
typedef intptr_t khronos_intptr_t;
|
||||
typedef uintptr_t khronos_uintptr_t;
|
||||
#elif defined(_WIN64)
|
||||
typedef signed long long int khronos_intptr_t;
|
||||
typedef unsigned long long int khronos_uintptr_t;
|
||||
#else
|
||||
typedef signed long int khronos_intptr_t;
|
||||
typedef unsigned long int khronos_uintptr_t;
|
||||
#endif
|
||||
|
||||
#if defined(_WIN64)
|
||||
typedef signed long long int khronos_ssize_t;
|
||||
typedef unsigned long long int khronos_usize_t;
|
||||
#else
|
||||
typedef signed long int khronos_ssize_t;
|
||||
typedef unsigned long int khronos_usize_t;
|
||||
#endif
|
||||
|
||||
#if KHRONOS_SUPPORT_FLOAT
|
||||
/*
|
||||
* Float type
|
||||
*/
|
||||
typedef float khronos_float_t;
|
||||
#endif
|
||||
|
||||
#if KHRONOS_SUPPORT_INT64
|
||||
/* Time types
|
||||
*
|
||||
* These types can be used to represent a time interval in nanoseconds or
|
||||
* an absolute Unadjusted System Time. Unadjusted System Time is the number
|
||||
* of nanoseconds since some arbitrary system event (e.g. since the last
|
||||
* time the system booted). The Unadjusted System Time is an unsigned
|
||||
* 64 bit value that wraps back to 0 every 584 years. Time intervals
|
||||
* may be either signed or unsigned.
|
||||
*/
|
||||
typedef khronos_uint64_t khronos_utime_nanoseconds_t;
|
||||
typedef khronos_int64_t khronos_stime_nanoseconds_t;
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Dummy value used to pad enum types to 32 bits.
|
||||
*/
|
||||
#ifndef KHRONOS_MAX_ENUM
|
||||
#define KHRONOS_MAX_ENUM 0x7FFFFFFF
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Enumerated boolean type
|
||||
*
|
||||
* Values other than zero should be considered to be true. Therefore
|
||||
* comparisons should not be made against KHRONOS_TRUE.
|
||||
*/
|
||||
typedef enum {
|
||||
KHRONOS_FALSE = 0,
|
||||
KHRONOS_TRUE = 1,
|
||||
KHRONOS_BOOLEAN_ENUM_FORCE_SIZE = KHRONOS_MAX_ENUM
|
||||
} khronos_boolean_enum_t;
|
||||
|
||||
#endif /* __khrplatform_h_ */
|
||||
3611
source/external/glad/include/glad/glad.h
vendored
Normal file
3611
source/external/glad/include/glad/glad.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1840
source/external/glad/src/glad.c
vendored
Normal file
1840
source/external/glad/src/glad.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
5631
source/external/stb_vorbis.c
vendored
Normal file
5631
source/external/stb_vorbis.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
source/external/stb_vorbis_impl.cpp
vendored
Normal file
7
source/external/stb_vorbis_impl.cpp
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// Isolated TU for the stb_vorbis implementation.
|
||||
// jail_audio.cpp defines STB_VORBIS_HEADER_ONLY before including stb_vorbis.c
|
||||
// (it only sees declarations there); this TU provides the definitions and
|
||||
// the linker resolves them.
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-suspicious-include)
|
||||
#include "stb_vorbis.c"
|
||||
509
source/main.cpp
Normal file
509
source/main.cpp
Normal file
@@ -0,0 +1,509 @@
|
||||
// src/main.cpp
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "audio/jail_audio.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "rendering/shader_backend.hpp"
|
||||
|
||||
struct Logger {
|
||||
static void info(const std::string& s) { std::cout << "[INFO] " << s << '\n'; }
|
||||
static void error(const std::string& s) { std::cerr << "[ERROR] " << s << '\n'; }
|
||||
};
|
||||
|
||||
struct VideoOptions {
|
||||
bool fullscreen = false;
|
||||
bool vsync = true;
|
||||
} Options_video;
|
||||
|
||||
struct DisplayMonitor {
|
||||
std::string name;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int refresh_rate = 0;
|
||||
};
|
||||
|
||||
static DisplayMonitor display_monitor_;
|
||||
static SDL_Window* window_ = nullptr;
|
||||
static std::unique_ptr<Rendering::IShaderBackend> backend_;
|
||||
|
||||
struct ShaderEntry {
|
||||
std::filesystem::path folder;
|
||||
std::string base_name;
|
||||
};
|
||||
|
||||
static std::vector<ShaderEntry> shader_list_;
|
||||
static std::vector<std::string> shader_names_;
|
||||
static std::vector<std::string> shader_authors_;
|
||||
static size_t current_shader_index_ = 0;
|
||||
static std::filesystem::path shaders_directory_;
|
||||
static Uint32 shader_start_ticks_ = 0;
|
||||
|
||||
static Uint32 fps_frame_count_ = 0;
|
||||
static Uint32 fps_last_update_ticks_ = 0;
|
||||
static float current_fps_ = 0.0f;
|
||||
|
||||
static std::unique_ptr<Ja::Engine> audio_engine_;
|
||||
static std::vector<Ja::Music*> music_list_;
|
||||
static std::vector<std::string> music_names_;
|
||||
static size_t current_music_index_ = 0;
|
||||
static bool music_muted_ = false;
|
||||
|
||||
static std::vector<ShaderEntry> scanShaderDirectory(const std::filesystem::path& directory) {
|
||||
std::vector<ShaderEntry> shaders;
|
||||
|
||||
if (!std::filesystem::exists(directory) || !std::filesystem::is_directory(directory)) {
|
||||
Logger::error("Shader directory does not exist: " + directory.string());
|
||||
return shaders;
|
||||
}
|
||||
|
||||
for (const auto& entry : std::filesystem::directory_iterator(directory)) {
|
||||
if (!entry.is_directory()) { continue; }
|
||||
|
||||
const std::string folder_name = entry.path().filename().string();
|
||||
if (folder_name.empty() || folder_name[0] == '_' || folder_name[0] == '.') { continue; }
|
||||
|
||||
const std::filesystem::path gl_source = entry.path() / (folder_name + ".gl.glsl");
|
||||
if (!std::filesystem::exists(gl_source)) {
|
||||
Logger::info("Skipping " + folder_name + ": missing " + gl_source.filename().string());
|
||||
continue;
|
||||
}
|
||||
|
||||
shaders.push_back(ShaderEntry{entry.path(), folder_name});
|
||||
}
|
||||
|
||||
std::sort(shaders.begin(), shaders.end(),
|
||||
[](const ShaderEntry& a, const ShaderEntry& b) { return a.base_name < b.base_name; });
|
||||
|
||||
Logger::info("Found " + std::to_string(shaders.size()) + " shader(s) in " + directory.string());
|
||||
|
||||
shader_names_.resize(shaders.size(), "");
|
||||
shader_authors_.resize(shaders.size(), "");
|
||||
|
||||
return shaders;
|
||||
}
|
||||
|
||||
static void preloadMusicDirectory(const std::filesystem::path& directory) {
|
||||
if (!std::filesystem::exists(directory) || !std::filesystem::is_directory(directory)) {
|
||||
Logger::info("Music directory does not exist: " + directory.string());
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> ogg_paths;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(directory)) {
|
||||
if (entry.is_regular_file() && entry.path().extension() == ".ogg") {
|
||||
ogg_paths.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
std::sort(ogg_paths.begin(), ogg_paths.end());
|
||||
|
||||
for (const auto& path : ogg_paths) {
|
||||
std::size_t size = 0;
|
||||
void* raw = SDL_LoadFile(path.string().c_str(), &size);
|
||||
if (raw == nullptr || size == 0) {
|
||||
Logger::error("Failed to read music file: " + path.string());
|
||||
if (raw != nullptr) { SDL_free(raw); }
|
||||
continue;
|
||||
}
|
||||
Ja::Music* music = Ja::loadMusic(static_cast<Uint8*>(raw), static_cast<Uint32>(size),
|
||||
path.filename().string().c_str());
|
||||
SDL_free(raw);
|
||||
if (music == nullptr) {
|
||||
Logger::error("Failed to decode OGG: " + path.string());
|
||||
continue;
|
||||
}
|
||||
music_list_.push_back(music);
|
||||
music_names_.push_back(path.filename().string());
|
||||
}
|
||||
|
||||
Logger::info("Preloaded " + std::to_string(music_list_.size()) + " music file(s) from " + directory.string());
|
||||
}
|
||||
|
||||
static void playRandomMusic() {
|
||||
if (music_list_.empty() || !audio_engine_) { return; }
|
||||
|
||||
current_music_index_ = static_cast<size_t>(rand()) % music_list_.size();
|
||||
audio_engine_->playMusic(music_list_[current_music_index_], 0);
|
||||
Logger::info("Now playing: " + music_names_[current_music_index_]);
|
||||
}
|
||||
|
||||
static void updateWindowTitle() {
|
||||
if (window_ == nullptr || shader_list_.empty()) { return; }
|
||||
|
||||
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_].base_name;
|
||||
}
|
||||
|
||||
if (!shader_authors_.empty() && !shader_authors_[current_shader_index_].empty()) {
|
||||
shaderName += " by " + shader_authors_[current_shader_index_];
|
||||
}
|
||||
|
||||
std::string title = WINDOW_TITLE_PREFIX;
|
||||
title += " (";
|
||||
title += shaderName;
|
||||
|
||||
if (backend_) {
|
||||
title += " - ";
|
||||
title += backend_->driverName();
|
||||
}
|
||||
|
||||
if (current_fps_ > 0.0f) {
|
||||
title += " - ";
|
||||
title += std::to_string(static_cast<int>(current_fps_ + 0.5f)) + " FPS";
|
||||
}
|
||||
|
||||
if (Options_video.vsync) {
|
||||
title += " - VSync";
|
||||
}
|
||||
title += ")";
|
||||
|
||||
SDL_SetWindowTitle(window_, title.c_str());
|
||||
}
|
||||
|
||||
static bool loadShaderAtIndex(size_t index) {
|
||||
if (index >= shader_list_.size()) {
|
||||
Logger::error("Invalid shader index: " + std::to_string(index));
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& entry = shader_list_[index];
|
||||
Logger::info("Loading shader: " + entry.folder.string());
|
||||
|
||||
Rendering::ShaderProgramSpec spec;
|
||||
spec.folder = entry.folder;
|
||||
spec.base_name = entry.base_name;
|
||||
|
||||
const std::filesystem::path meta_path = entry.folder / "meta.txt";
|
||||
if (std::filesystem::exists(meta_path)) {
|
||||
spec.metadata = Rendering::parseMetaFile(meta_path);
|
||||
} else {
|
||||
std::string source;
|
||||
const std::filesystem::path gl_source = entry.folder / (entry.base_name + ".gl.glsl");
|
||||
if (Rendering::loadFileToString(gl_source, source)) {
|
||||
spec.metadata = Rendering::extractShaderMetadata(source);
|
||||
}
|
||||
}
|
||||
|
||||
if (!spec.metadata.name.empty()) {
|
||||
shader_names_[index] = spec.metadata.name;
|
||||
Logger::info("Shader name: " + spec.metadata.name);
|
||||
}
|
||||
if (!spec.metadata.author.empty()) {
|
||||
shader_authors_[index] = spec.metadata.author;
|
||||
Logger::info("Shader author: " + spec.metadata.author);
|
||||
}
|
||||
|
||||
return backend_->loadShader(spec);
|
||||
}
|
||||
|
||||
void getDisplayInfo() {
|
||||
int num_displays = 0;
|
||||
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
|
||||
if (displays != nullptr && num_displays > 0) {
|
||||
for (int i = 0; i < num_displays; ++i) {
|
||||
const SDL_DisplayID instance_id = displays[i];
|
||||
const char* name = SDL_GetDisplayName(instance_id);
|
||||
Logger::info(std::string("Display ") + std::to_string(instance_id) + ": " + (name != nullptr ? name : "Unknown"));
|
||||
}
|
||||
|
||||
const SDL_DisplayMode* dm = SDL_GetCurrentDisplayMode(displays[0]);
|
||||
const char* first_display_name = SDL_GetDisplayName(displays[0]);
|
||||
display_monitor_.name = (first_display_name != nullptr) ? first_display_name : "Unknown";
|
||||
if (dm != nullptr) {
|
||||
display_monitor_.width = static_cast<int>(dm->w);
|
||||
display_monitor_.height = static_cast<int>(dm->h);
|
||||
display_monitor_.refresh_rate = static_cast<int>(dm->refresh_rate);
|
||||
} else {
|
||||
Logger::info("SDL_GetCurrentDisplayMode returned null");
|
||||
}
|
||||
} else {
|
||||
Logger::info("No displays found or SDL_GetDisplays failed");
|
||||
}
|
||||
}
|
||||
|
||||
void setFullscreenMode() {
|
||||
if (window_ == nullptr) { return; }
|
||||
|
||||
if (Options_video.fullscreen) {
|
||||
if (!SDL_SetWindowFullscreen(window_, true)) {
|
||||
Logger::error(std::string("Failed to set fullscreen: ") + SDL_GetError());
|
||||
Logger::info("Fallback to windowed mode 800x800");
|
||||
SDL_SetWindowFullscreen(window_, false);
|
||||
SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||
Options_video.fullscreen = false;
|
||||
SDL_ShowCursor();
|
||||
} else {
|
||||
SDL_HideCursor();
|
||||
}
|
||||
} else {
|
||||
SDL_SetWindowFullscreen(window_, false);
|
||||
SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||
SDL_ShowCursor();
|
||||
}
|
||||
}
|
||||
|
||||
void toggleFullscreen() {
|
||||
Options_video.fullscreen = !Options_video.fullscreen;
|
||||
setFullscreenMode();
|
||||
}
|
||||
|
||||
void toggleVSync() {
|
||||
Options_video.vsync = !Options_video.vsync;
|
||||
if (backend_) {
|
||||
backend_->setVSync(Options_video.vsync);
|
||||
}
|
||||
}
|
||||
|
||||
void switchShader(int direction) {
|
||||
if (shader_list_.empty()) { return; }
|
||||
|
||||
size_t new_index = current_shader_index_;
|
||||
if (direction > 0) {
|
||||
new_index = (current_shader_index_ + 1) % shader_list_.size();
|
||||
} else if (direction < 0) {
|
||||
new_index = (current_shader_index_ == 0) ? shader_list_.size() - 1 : current_shader_index_ - 1;
|
||||
}
|
||||
|
||||
if (!loadShaderAtIndex(new_index)) {
|
||||
Logger::error("Failed to switch shader, keeping current one");
|
||||
return;
|
||||
}
|
||||
|
||||
current_shader_index_ = new_index;
|
||||
shader_start_ticks_ = SDL_GetTicks();
|
||||
updateWindowTitle();
|
||||
}
|
||||
|
||||
void handleDebugEvents(const SDL_Event& event) {
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && static_cast<int>(event.key.repeat) == 0) {
|
||||
switch (event.key.key) {
|
||||
case SDLK_F3: { toggleFullscreen(); break; }
|
||||
case SDLK_F4: { toggleVSync(); break; }
|
||||
case SDLK_M: {
|
||||
music_muted_ = !music_muted_;
|
||||
if (audio_engine_) { audio_engine_->setMusicVolume(music_muted_ ? 0.0f : 1.0f); }
|
||||
Logger::info(music_muted_ ? "Music muted" : "Music unmuted");
|
||||
break;
|
||||
}
|
||||
case SDLK_LEFT: { switchShader(-1); break; }
|
||||
case SDLK_RIGHT: { switchShader(+1); break; }
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class BackendChoice { Auto, Gpu, OpenGL };
|
||||
|
||||
static auto createWindowForBackend(BackendChoice choice) -> SDL_Window* {
|
||||
SDL_WindowFlags flags = SDL_WINDOW_RESIZABLE;
|
||||
if (choice == BackendChoice::OpenGL) {
|
||||
flags |= SDL_WINDOW_OPENGL;
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
}
|
||||
return SDL_CreateWindow(APP_NAME, WINDOW_WIDTH, WINDOW_HEIGHT, flags);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
std::string shaderPath;
|
||||
bool fullscreenFlag = false;
|
||||
BackendChoice backend_choice = BackendChoice::Auto;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const std::string a = argv[i];
|
||||
if (a == "-F" || a == "--fullscreen") { fullscreenFlag = true; continue; }
|
||||
if (a == "--backend=gpu") { backend_choice = BackendChoice::Gpu; continue; }
|
||||
if (a == "--backend=opengl") { backend_choice = BackendChoice::OpenGL; continue; }
|
||||
if (a == "--backend=auto") { backend_choice = BackendChoice::Auto; continue; }
|
||||
if (shaderPath.empty()) { shaderPath = a; }
|
||||
}
|
||||
if (shaderPath.empty()) { shaderPath = "test"; }
|
||||
Options_video.fullscreen = fullscreenFlag;
|
||||
|
||||
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 {
|
||||
if (initResult != 0) { Logger::error(SDL_GetError()); return -1; }
|
||||
}
|
||||
|
||||
getDisplayInfo();
|
||||
|
||||
if (backend_choice != BackendChoice::OpenGL) {
|
||||
window_ = createWindowForBackend(BackendChoice::Gpu);
|
||||
if (window_ != nullptr) {
|
||||
backend_ = Rendering::makeSdl3GpuBackend();
|
||||
if (!backend_->init(window_)) {
|
||||
Logger::info("SDL3 GPU backend init failed, falling back to OpenGL");
|
||||
backend_.reset();
|
||||
SDL_DestroyWindow(window_);
|
||||
window_ = nullptr;
|
||||
if (backend_choice == BackendChoice::Gpu) {
|
||||
SDL_Quit();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (backend_ == nullptr) {
|
||||
window_ = createWindowForBackend(BackendChoice::OpenGL);
|
||||
if (window_ == nullptr) {
|
||||
Logger::error(std::string("SDL_CreateWindow error: ") + SDL_GetError());
|
||||
SDL_Quit();
|
||||
return -1;
|
||||
}
|
||||
backend_ = Rendering::makeOpenGLBackend();
|
||||
if (!backend_->init(window_)) {
|
||||
Logger::error("Failed to initialize shader backend");
|
||||
SDL_DestroyWindow(window_);
|
||||
SDL_Quit();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
setFullscreenMode();
|
||||
backend_->setVSync(Options_video.vsync);
|
||||
|
||||
audio_engine_ = std::make_unique<Ja::Engine>(48000, SDL_AUDIO_S16, 2);
|
||||
audio_engine_->setOnMusicEnded([]() { playRandomMusic(); });
|
||||
|
||||
const std::string resources_dir = getResourcesDirectory();
|
||||
|
||||
srand(static_cast<unsigned int>(time(nullptr)));
|
||||
|
||||
const std::filesystem::path music_directory = std::filesystem::path(resources_dir) / "data" / "music";
|
||||
preloadMusicDirectory(music_directory);
|
||||
|
||||
if (!music_list_.empty()) {
|
||||
playRandomMusic();
|
||||
} else {
|
||||
Logger::info("No music files found in " + music_directory.string());
|
||||
}
|
||||
|
||||
const std::filesystem::path arg_path(shaderPath);
|
||||
std::filesystem::path target_folder;
|
||||
if (arg_path.has_parent_path()) {
|
||||
target_folder = arg_path;
|
||||
shaders_directory_ = arg_path.parent_path();
|
||||
} else {
|
||||
shaders_directory_ = std::filesystem::path(resources_dir) / "data" / "shaders";
|
||||
target_folder = shaders_directory_ / shaderPath;
|
||||
}
|
||||
|
||||
shader_list_ = scanShaderDirectory(shaders_directory_);
|
||||
if (shader_list_.empty()) {
|
||||
Logger::error("No shaders found in directory: " + shaders_directory_.string());
|
||||
backend_->cleanup();
|
||||
SDL_DestroyWindow(window_);
|
||||
SDL_Quit();
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t initial_index = 0;
|
||||
bool found_shader = false;
|
||||
|
||||
for (size_t i = 0; i < shader_list_.size(); ++i) {
|
||||
if (shader_list_[i].folder == target_folder) {
|
||||
initial_index = i;
|
||||
found_shader = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_shader) {
|
||||
const std::filesystem::path default_folder = std::filesystem::path(resources_dir) / "data" / "shaders" / "test";
|
||||
for (size_t i = 0; i < shader_list_.size(); ++i) {
|
||||
if (shader_list_[i].folder == default_folder) {
|
||||
initial_index = i;
|
||||
found_shader = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_shader) {
|
||||
Logger::info("Specified shader not found, using first shader in directory");
|
||||
initial_index = 0;
|
||||
}
|
||||
|
||||
current_shader_index_ = initial_index;
|
||||
if (!loadShaderAtIndex(current_shader_index_)) {
|
||||
Logger::error("Failed to load initial shader");
|
||||
backend_->cleanup();
|
||||
SDL_DestroyWindow(window_);
|
||||
SDL_Quit();
|
||||
return -1;
|
||||
}
|
||||
|
||||
shader_start_ticks_ = SDL_GetTicks();
|
||||
fps_last_update_ticks_ = SDL_GetTicks();
|
||||
updateWindowTitle();
|
||||
|
||||
bool running = true;
|
||||
|
||||
while (running) {
|
||||
fps_frame_count_++;
|
||||
const Uint32 current_ticks = SDL_GetTicks();
|
||||
|
||||
if (current_ticks - fps_last_update_ticks_ >= 500) {
|
||||
const float elapsed_seconds = static_cast<float>(current_ticks - fps_last_update_ticks_) / 1000.0f;
|
||||
current_fps_ = static_cast<float>(fps_frame_count_) / elapsed_seconds;
|
||||
fps_frame_count_ = 0;
|
||||
fps_last_update_ticks_ = current_ticks;
|
||||
updateWindowTitle();
|
||||
}
|
||||
|
||||
if (audio_engine_) { audio_engine_->update(); }
|
||||
|
||||
SDL_Event e;
|
||||
while (SDL_PollEvent(&e)) {
|
||||
if (e.type == SDL_EVENT_QUIT) {
|
||||
running = false;
|
||||
} else if (e.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
|
||||
running = false;
|
||||
} else if (e.type == SDL_EVENT_KEY_DOWN) {
|
||||
if (e.key.key == SDLK_ESCAPE) { running = false; }
|
||||
handleDebugEvents(e);
|
||||
}
|
||||
}
|
||||
|
||||
Rendering::ShaderUniforms uniforms;
|
||||
uniforms.iTime = static_cast<float>(SDL_GetTicks() - shader_start_ticks_) / 1000.0f;
|
||||
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
SDL_GetWindowSize(window_, &w, &h);
|
||||
uniforms.iResolutionX = static_cast<float>(w);
|
||||
uniforms.iResolutionY = static_cast<float>(h);
|
||||
|
||||
backend_->render(uniforms);
|
||||
|
||||
if (!Options_video.vsync) {
|
||||
SDL_Delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
backend_->cleanup();
|
||||
backend_.reset();
|
||||
|
||||
for (Ja::Music* m : music_list_) { Ja::deleteMusic(m); }
|
||||
music_list_.clear();
|
||||
music_names_.clear();
|
||||
audio_engine_.reset();
|
||||
|
||||
SDL_DestroyWindow(window_);
|
||||
SDL_Quit();
|
||||
return 0;
|
||||
}
|
||||
297
source/rendering/opengl_shader_backend.cpp
Normal file
297
source/rendering/opengl_shader_backend.cpp
Normal file
@@ -0,0 +1,297 @@
|
||||
#include "rendering/opengl_shader_backend.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* VERTEX_SHADER_SRC = R"glsl(
|
||||
#version 330 core
|
||||
layout(location = 0) in vec2 aPos;
|
||||
out vec2 vUV;
|
||||
void main() {
|
||||
vUV = aPos * 0.5 + 0.5;
|
||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||
}
|
||||
)glsl";
|
||||
|
||||
void logInfo(const std::string& msg) { std::cout << "[INFO] " << msg << '\n'; }
|
||||
void logError(const std::string& msg) { std::cerr << "[ERROR] " << msg << '\n'; }
|
||||
|
||||
auto compileShader(GLenum type, const char* src) -> GLuint {
|
||||
const GLuint s = glCreateShader(type);
|
||||
glShaderSource(s, 1, &src, nullptr);
|
||||
glCompileShader(s);
|
||||
GLint ok = 0;
|
||||
glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
|
||||
if (ok == 0) {
|
||||
GLint len = 0;
|
||||
glGetShaderiv(s, GL_INFO_LOG_LENGTH, &len);
|
||||
std::string log(len > 0 ? len : 1, ' ');
|
||||
glGetShaderInfoLog(s, len, nullptr, log.data());
|
||||
logError("Shader compile error: " + log);
|
||||
glDeleteShader(s);
|
||||
return 0;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
auto linkProgram(GLuint vs, GLuint fs) -> GLuint {
|
||||
const GLuint p = glCreateProgram();
|
||||
glAttachShader(p, vs);
|
||||
glAttachShader(p, fs);
|
||||
glLinkProgram(p);
|
||||
GLint ok = 0;
|
||||
glGetProgramiv(p, GL_LINK_STATUS, &ok);
|
||||
if (ok == 0) {
|
||||
GLint len = 0;
|
||||
glGetProgramiv(p, GL_INFO_LOG_LENGTH, &len);
|
||||
std::string log(len > 0 ? len : 1, ' ');
|
||||
glGetProgramInfoLog(p, len, nullptr, log.data());
|
||||
logError("Program link error: " + log);
|
||||
glDeleteProgram(p);
|
||||
return 0;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
auto detectFeedbackChannel(const ShaderMetadata& metadata) -> int {
|
||||
if (metadata.iChannel0 == "self") { return 0; }
|
||||
if (metadata.iChannel1 == "self") { return 1; }
|
||||
if (metadata.iChannel2 == "self") { return 2; }
|
||||
if (metadata.iChannel3 == "self") { return 3; }
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
OpenGLShaderBackend::~OpenGLShaderBackend() { cleanup(); }
|
||||
|
||||
auto OpenGLShaderBackend::init(SDL_Window* window) -> bool {
|
||||
window_ = window;
|
||||
|
||||
gl_context_ = SDL_GL_CreateContext(window_);
|
||||
if (gl_context_ == nullptr) {
|
||||
logError(std::string("SDL_GL_CreateContext error: ") + SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gladLoadGLLoader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress)) == 0) {
|
||||
logError("Failed to initialize GL loader");
|
||||
SDL_GL_DestroyContext(gl_context_);
|
||||
gl_context_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr float QUAD_VERTICES[] = {
|
||||
-1.0f, -1.0f,
|
||||
1.0f, -1.0f,
|
||||
-1.0f, 1.0f,
|
||||
1.0f, 1.0f,
|
||||
};
|
||||
|
||||
glGenVertexArrays(1, &vao_);
|
||||
glGenBuffers(1, &vbo_);
|
||||
glBindVertexArray(vao_);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(QUAD_VERTICES), QUAD_VERTICES, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto OpenGLShaderBackend::loadShader(const ShaderProgramSpec& spec) -> bool {
|
||||
const std::filesystem::path source_path = spec.folder / (spec.base_name + ".gl.glsl");
|
||||
|
||||
std::string fragSrc;
|
||||
if (!loadFileToString(source_path, fragSrc)) {
|
||||
logError("Failed to load shader file: " + source_path.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
const int feedback = detectFeedbackChannel(spec.metadata);
|
||||
|
||||
const GLuint vs = compileShader(GL_VERTEX_SHADER, VERTEX_SHADER_SRC);
|
||||
const GLuint fs = compileShader(GL_FRAGMENT_SHADER, fragSrc.c_str());
|
||||
|
||||
if (vs == 0 || fs == 0) {
|
||||
if (vs != 0) { glDeleteShader(vs); }
|
||||
if (fs != 0) { glDeleteShader(fs); }
|
||||
logError("Shader compilation failed for: " + source_path.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
const GLuint program = linkProgram(vs, fs);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
|
||||
if (program == 0) {
|
||||
logError("Program linking failed for: " + source_path.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current_program_ != 0) {
|
||||
glDeleteProgram(current_program_);
|
||||
}
|
||||
current_program_ = program;
|
||||
|
||||
destroyFeedbackFBO();
|
||||
feedback_channel_ = feedback;
|
||||
current_shader_uses_feedback_ = (feedback >= 0);
|
||||
|
||||
if (current_shader_uses_feedback_) {
|
||||
logInfo("Shader uses self-feedback on iChannel" + std::to_string(feedback_channel_));
|
||||
}
|
||||
|
||||
logInfo("Shader loaded successfully: " + spec.base_name);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto OpenGLShaderBackend::createFeedbackFBO(int width, int height) -> bool {
|
||||
destroyFeedbackFBO();
|
||||
|
||||
glGenTextures(1, &feedback_texture_);
|
||||
glBindTexture(GL_TEXTURE_2D, feedback_texture_);
|
||||
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);
|
||||
|
||||
std::vector<float> black(static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4U, 0.0f);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, black.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
glGenFramebuffers(1, &feedback_fbo_);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, feedback_fbo_);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, feedback_texture_, 0);
|
||||
|
||||
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
logError("Feedback FBO creation failed: " + std::to_string(status));
|
||||
destroyFeedbackFBO();
|
||||
return false;
|
||||
}
|
||||
|
||||
feedback_width_ = width;
|
||||
feedback_height_ = height;
|
||||
logInfo("Created feedback FBO (" + std::to_string(width) + "x" + std::to_string(height) + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenGLShaderBackend::destroyFeedbackFBO() {
|
||||
if (feedback_fbo_ != 0) {
|
||||
glDeleteFramebuffers(1, &feedback_fbo_);
|
||||
feedback_fbo_ = 0;
|
||||
}
|
||||
if (feedback_texture_ != 0) {
|
||||
glDeleteTextures(1, &feedback_texture_);
|
||||
feedback_texture_ = 0;
|
||||
}
|
||||
feedback_width_ = 0;
|
||||
feedback_height_ = 0;
|
||||
}
|
||||
|
||||
void OpenGLShaderBackend::render(const ShaderUniforms& uniforms) {
|
||||
if (current_program_ == 0 || window_ == nullptr) { return; }
|
||||
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
SDL_GetWindowSize(window_, &w, &h);
|
||||
|
||||
if (current_shader_uses_feedback_) {
|
||||
if (feedback_fbo_ == 0 || feedback_width_ != w || feedback_height_ != h) {
|
||||
createFeedbackFBO(w, h);
|
||||
}
|
||||
}
|
||||
|
||||
glUseProgram(current_program_);
|
||||
|
||||
const GLint locRes = glGetUniformLocation(current_program_, "iResolution");
|
||||
const GLint locTime = glGetUniformLocation(current_program_, "iTime");
|
||||
|
||||
if (current_shader_uses_feedback_) {
|
||||
const std::string channel_name = "iChannel" + std::to_string(feedback_channel_);
|
||||
const GLint locChannel = glGetUniformLocation(current_program_, channel_name.c_str());
|
||||
|
||||
if (locChannel >= 0) {
|
||||
glActiveTexture(GL_TEXTURE0 + feedback_channel_);
|
||||
glBindTexture(GL_TEXTURE_2D, feedback_texture_);
|
||||
glUniform1i(locChannel, feedback_channel_);
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, feedback_fbo_);
|
||||
glViewport(0, 0, w, h);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
if (locRes >= 0) { glUniform2f(locRes, static_cast<float>(w), static_cast<float>(h)); }
|
||||
if (locTime >= 0) { glUniform1f(locTime, uniforms.iTime); }
|
||||
|
||||
glBindVertexArray(vao_);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glViewport(0, 0, w, h);
|
||||
|
||||
if (locRes >= 0) { glUniform2f(locRes, static_cast<float>(w), static_cast<float>(h)); }
|
||||
if (locTime >= 0) { glUniform1f(locTime, uniforms.iTime); }
|
||||
|
||||
glBindVertexArray(vao_);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0 + feedback_channel_);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
} else {
|
||||
glViewport(0, 0, w, h);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
if (locRes >= 0) { glUniform2f(locRes, static_cast<float>(w), static_cast<float>(h)); }
|
||||
if (locTime >= 0) { glUniform1f(locTime, uniforms.iTime); }
|
||||
|
||||
glBindVertexArray(vao_);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
SDL_GL_SwapWindow(window_);
|
||||
}
|
||||
|
||||
void OpenGLShaderBackend::setVSync(bool vsync) {
|
||||
const int result = SDL_GL_SetSwapInterval(vsync ? 1 : 0);
|
||||
if (result == 0) {
|
||||
logInfo(vsync ? "VSync enabled" : "VSync disabled");
|
||||
} else {
|
||||
logError(std::string("Failed to set VSync: ") + SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
void OpenGLShaderBackend::cleanup() {
|
||||
if (gl_context_ == nullptr) { return; }
|
||||
|
||||
if (vbo_ != 0) { glDeleteBuffers(1, &vbo_); vbo_ = 0; }
|
||||
if (vao_ != 0) { glDeleteVertexArrays(1, &vao_); vao_ = 0; }
|
||||
if (current_program_ != 0) { glDeleteProgram(current_program_); current_program_ = 0; }
|
||||
destroyFeedbackFBO();
|
||||
current_shader_uses_feedback_ = false;
|
||||
feedback_channel_ = -1;
|
||||
|
||||
SDL_GL_DestroyContext(gl_context_);
|
||||
gl_context_ = nullptr;
|
||||
window_ = nullptr;
|
||||
}
|
||||
|
||||
auto makeOpenGLBackend() -> std::unique_ptr<IShaderBackend> {
|
||||
return std::make_unique<OpenGLShaderBackend>();
|
||||
}
|
||||
|
||||
} // namespace Rendering
|
||||
40
source/rendering/opengl_shader_backend.hpp
Normal file
40
source/rendering/opengl_shader_backend.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <glad/glad.h>
|
||||
|
||||
#include "rendering/shader_backend.hpp"
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
class OpenGLShaderBackend final : public IShaderBackend {
|
||||
public:
|
||||
OpenGLShaderBackend() = default;
|
||||
~OpenGLShaderBackend() override;
|
||||
|
||||
auto init(SDL_Window* window) -> bool override;
|
||||
auto loadShader(const ShaderProgramSpec& spec) -> bool override;
|
||||
void render(const ShaderUniforms& uniforms) override;
|
||||
void setVSync(bool vsync) override;
|
||||
void cleanup() override;
|
||||
[[nodiscard]] auto driverName() const -> std::string override { return "OpenGL"; }
|
||||
|
||||
private:
|
||||
auto createFeedbackFBO(int width, int height) -> bool;
|
||||
void destroyFeedbackFBO();
|
||||
|
||||
SDL_Window* window_{nullptr};
|
||||
SDL_GLContext gl_context_{nullptr};
|
||||
|
||||
GLuint vao_{0};
|
||||
GLuint vbo_{0};
|
||||
GLuint current_program_{0};
|
||||
|
||||
GLuint feedback_fbo_{0};
|
||||
GLuint feedback_texture_{0};
|
||||
bool current_shader_uses_feedback_{false};
|
||||
int feedback_channel_{-1};
|
||||
int feedback_width_{0};
|
||||
int feedback_height_{0};
|
||||
};
|
||||
|
||||
} // namespace Rendering
|
||||
238
source/rendering/sdl3gpu/sdl3gpu_shader_backend.cpp
Normal file
238
source/rendering/sdl3gpu/sdl3gpu_shader_backend.cpp
Normal file
@@ -0,0 +1,238 @@
|
||||
#include "rendering/sdl3gpu/sdl3gpu_shader_backend.hpp"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "rendering/sdl3gpu/shader_factory.hpp"
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
namespace {
|
||||
|
||||
void logInfo(const std::string& msg) { std::cout << "[INFO] " << msg << '\n'; }
|
||||
void logError(const std::string& msg) { std::cerr << "[ERROR] " << msg << '\n'; }
|
||||
|
||||
#ifdef __APPLE__
|
||||
constexpr SDL_GPUShaderFormat SHADER_FORMAT = SDL_GPU_SHADERFORMAT_MSL;
|
||||
constexpr const char* VERTEX_ENTRY = "passthrough_vs";
|
||||
constexpr const char* FRAGMENT_ENTRY = "test_fs"; // overridden per-shader (see loadShader)
|
||||
constexpr const char* VERTEX_SUFFIX = ".vert.msl";
|
||||
constexpr const char* FRAGMENT_SUFFIX = ".frag.msl";
|
||||
#else
|
||||
constexpr SDL_GPUShaderFormat SHADER_FORMAT = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||
constexpr const char* VERTEX_ENTRY = "main";
|
||||
constexpr const char* FRAGMENT_ENTRY = "main";
|
||||
constexpr const char* VERTEX_SUFFIX = ".vert.spv";
|
||||
constexpr const char* FRAGMENT_SUFFIX = ".frag.spv";
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
Sdl3GpuShaderBackend::~Sdl3GpuShaderBackend() { cleanup(); }
|
||||
|
||||
auto Sdl3GpuShaderBackend::init(SDL_Window* window) -> bool {
|
||||
window_ = window;
|
||||
return createDevice();
|
||||
}
|
||||
|
||||
auto Sdl3GpuShaderBackend::createDevice() -> bool {
|
||||
device_ = SDL_CreateGPUDevice(SHADER_FORMAT, false, nullptr);
|
||||
if (device_ == nullptr) {
|
||||
logError(std::string("SDL_CreateGPUDevice failed: ") + SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
|
||||
logError(std::string("SDL_ClaimWindowForGPUDevice failed: ") + SDL_GetError());
|
||||
SDL_DestroyGPUDevice(device_);
|
||||
device_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode());
|
||||
|
||||
const char* name = SDL_GetGPUDeviceDriver(device_);
|
||||
const std::string raw = (name != nullptr) ? name : "GPU";
|
||||
if (raw == "vulkan") { driver_name_ = "Vulkan"; }
|
||||
else if (raw == "metal") { driver_name_ = "Metal"; }
|
||||
else if (raw == "d3d12") { driver_name_ = "D3D12"; }
|
||||
else if (!raw.empty()) {
|
||||
driver_name_ = raw;
|
||||
driver_name_[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(driver_name_[0])));
|
||||
} else {
|
||||
driver_name_ = "GPU";
|
||||
}
|
||||
logInfo("GPU driver: " + driver_name_);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Sdl3GpuShaderBackend::loadVertexShaderFor(const ShaderProgramSpec& spec) -> bool {
|
||||
if (vertex_shader_ != nullptr) { return true; }
|
||||
|
||||
const std::filesystem::path common_dir = spec.folder.parent_path() / "_common";
|
||||
const std::filesystem::path vertex_path = common_dir / (std::string("passthrough") + VERTEX_SUFFIX);
|
||||
|
||||
vertex_shader_ = Sdl3Gpu::loadShaderFromFile(device_, vertex_path, SHADER_FORMAT,
|
||||
VERTEX_ENTRY, SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
if (vertex_shader_ == nullptr) {
|
||||
logError("Failed to load shared vertex shader: " + vertex_path.string() + " (" + SDL_GetError() + ")");
|
||||
return false;
|
||||
}
|
||||
logInfo("Loaded shared vertex shader: " + vertex_path.filename().string());
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Sdl3GpuShaderBackend::buildPipeline(SDL_GPUShader* fragment) -> SDL_GPUGraphicsPipeline* {
|
||||
const SDL_GPUTextureFormat SWAPCHAIN_FORMAT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||
|
||||
SDL_GPUColorTargetBlendState no_blend{};
|
||||
SDL_GPUColorTargetDescription color_target{};
|
||||
color_target.format = SWAPCHAIN_FORMAT;
|
||||
color_target.blend_state = no_blend;
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo info{};
|
||||
info.vertex_shader = vertex_shader_;
|
||||
info.fragment_shader = fragment;
|
||||
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
info.target_info.num_color_targets = 1;
|
||||
info.target_info.color_target_descriptions = &color_target;
|
||||
|
||||
SDL_GPUGraphicsPipeline* pipeline = SDL_CreateGPUGraphicsPipeline(device_, &info);
|
||||
if (pipeline == nullptr) {
|
||||
logError(std::string("SDL_CreateGPUGraphicsPipeline failed: ") + SDL_GetError());
|
||||
}
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
auto Sdl3GpuShaderBackend::loadShader(const ShaderProgramSpec& spec) -> bool {
|
||||
if (device_ == nullptr) { return false; }
|
||||
|
||||
if (!loadVertexShaderFor(spec)) { return false; }
|
||||
|
||||
const std::filesystem::path frag_path = spec.folder / (spec.base_name + FRAGMENT_SUFFIX);
|
||||
|
||||
#ifdef __APPLE__
|
||||
const std::string entry = spec.base_name + "_fs";
|
||||
const char* fragment_entry = entry.c_str();
|
||||
#else
|
||||
const char* fragment_entry = FRAGMENT_ENTRY;
|
||||
#endif
|
||||
|
||||
SDL_GPUShader* new_fragment = Sdl3Gpu::loadShaderFromFile(device_, frag_path, SHADER_FORMAT,
|
||||
fragment_entry, SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 1);
|
||||
if (new_fragment == nullptr) {
|
||||
logError("Failed to load fragment shader: " + frag_path.string() + " (" + SDL_GetError() + ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_GPUGraphicsPipeline* new_pipeline = buildPipeline(new_fragment);
|
||||
if (new_pipeline == nullptr) {
|
||||
SDL_ReleaseGPUShader(device_, new_fragment);
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_WaitForGPUIdle(device_);
|
||||
if (pipeline_ != nullptr) { SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_); }
|
||||
if (fragment_shader_ != nullptr) { SDL_ReleaseGPUShader(device_, fragment_shader_); }
|
||||
|
||||
pipeline_ = new_pipeline;
|
||||
fragment_shader_ = new_fragment;
|
||||
logInfo("Shader loaded successfully: " + spec.base_name);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Sdl3GpuShaderBackend::render(const ShaderUniforms& uniforms) {
|
||||
if (device_ == nullptr || pipeline_ == nullptr) { return; }
|
||||
|
||||
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
|
||||
if (cmd == nullptr) { return; }
|
||||
|
||||
SDL_GPUTexture* swapchain = nullptr;
|
||||
Uint32 sw = 0;
|
||||
Uint32 sh = 0;
|
||||
if (!SDL_AcquireGPUSwapchainTexture(cmd, window_, &swapchain, &sw, &sh) || swapchain == nullptr) {
|
||||
SDL_SubmitGPUCommandBuffer(cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_GPUColorTargetInfo color_target{};
|
||||
color_target.texture = swapchain;
|
||||
color_target.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||
color_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
color_target.clear_color = {.r = 0.0f, .g = 0.0f, .b = 0.0f, .a = 1.0f};
|
||||
|
||||
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr);
|
||||
if (pass != nullptr) {
|
||||
SDL_GPUViewport vp{};
|
||||
vp.x = 0.0f;
|
||||
vp.y = 0.0f;
|
||||
vp.w = static_cast<float>(sw);
|
||||
vp.h = static_cast<float>(sh);
|
||||
vp.min_depth = 0.0f;
|
||||
vp.max_depth = 1.0f;
|
||||
SDL_SetGPUViewport(pass, &vp);
|
||||
|
||||
SDL_BindGPUGraphicsPipeline(pass, pipeline_);
|
||||
|
||||
UniformsStd140 ubo{};
|
||||
ubo.iTime = uniforms.iTime;
|
||||
ubo.iResolutionX = uniforms.iResolutionX;
|
||||
ubo.iResolutionY = uniforms.iResolutionY;
|
||||
SDL_PushGPUFragmentUniformData(cmd, 0, &ubo, sizeof(ubo));
|
||||
|
||||
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
|
||||
SDL_EndGPURenderPass(pass);
|
||||
}
|
||||
|
||||
SDL_SubmitGPUCommandBuffer(cmd);
|
||||
}
|
||||
|
||||
void Sdl3GpuShaderBackend::setVSync(bool vsync) {
|
||||
vsync_ = vsync;
|
||||
if (device_ != nullptr && window_ != nullptr) {
|
||||
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode());
|
||||
logInfo(vsync ? "VSync enabled" : "VSync disabled");
|
||||
}
|
||||
}
|
||||
|
||||
auto Sdl3GpuShaderBackend::bestPresentMode() const -> SDL_GPUPresentMode {
|
||||
if (vsync_) { return SDL_GPU_PRESENTMODE_VSYNC; }
|
||||
if (device_ != nullptr && window_ != nullptr) {
|
||||
if (SDL_WindowSupportsGPUPresentMode(device_, window_, SDL_GPU_PRESENTMODE_IMMEDIATE)) {
|
||||
return SDL_GPU_PRESENTMODE_IMMEDIATE;
|
||||
}
|
||||
if (SDL_WindowSupportsGPUPresentMode(device_, window_, SDL_GPU_PRESENTMODE_MAILBOX)) {
|
||||
return SDL_GPU_PRESENTMODE_MAILBOX;
|
||||
}
|
||||
}
|
||||
return SDL_GPU_PRESENTMODE_VSYNC;
|
||||
}
|
||||
|
||||
void Sdl3GpuShaderBackend::cleanup() {
|
||||
if (device_ == nullptr) { return; }
|
||||
|
||||
SDL_WaitForGPUIdle(device_);
|
||||
|
||||
if (pipeline_ != nullptr) {
|
||||
SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_);
|
||||
pipeline_ = nullptr;
|
||||
}
|
||||
if (fragment_shader_ != nullptr) {
|
||||
SDL_ReleaseGPUShader(device_, fragment_shader_);
|
||||
fragment_shader_ = nullptr;
|
||||
}
|
||||
if (vertex_shader_ != nullptr) {
|
||||
SDL_ReleaseGPUShader(device_, vertex_shader_);
|
||||
vertex_shader_ = nullptr;
|
||||
}
|
||||
|
||||
if (window_ != nullptr) {
|
||||
SDL_ReleaseWindowFromGPUDevice(device_, window_);
|
||||
}
|
||||
SDL_DestroyGPUDevice(device_);
|
||||
device_ = nullptr;
|
||||
window_ = nullptr;
|
||||
}
|
||||
|
||||
auto makeSdl3GpuBackend() -> std::unique_ptr<IShaderBackend> {
|
||||
return std::make_unique<Sdl3GpuShaderBackend>();
|
||||
}
|
||||
|
||||
} // namespace Rendering
|
||||
44
source/rendering/sdl3gpu/sdl3gpu_shader_backend.hpp
Normal file
44
source/rendering/sdl3gpu/sdl3gpu_shader_backend.hpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "rendering/shader_backend.hpp"
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
class Sdl3GpuShaderBackend final : public IShaderBackend {
|
||||
public:
|
||||
Sdl3GpuShaderBackend() = default;
|
||||
~Sdl3GpuShaderBackend() override;
|
||||
|
||||
auto init(SDL_Window* window) -> bool override;
|
||||
auto loadShader(const ShaderProgramSpec& spec) -> bool override;
|
||||
void render(const ShaderUniforms& uniforms) override;
|
||||
void setVSync(bool vsync) override;
|
||||
void cleanup() override;
|
||||
[[nodiscard]] auto driverName() const -> std::string override { return driver_name_; }
|
||||
|
||||
private:
|
||||
struct UniformsStd140 {
|
||||
float iTime{0.0f};
|
||||
float pad0{0.0f};
|
||||
float iResolutionX{0.0f};
|
||||
float iResolutionY{0.0f};
|
||||
};
|
||||
|
||||
auto createDevice() -> bool;
|
||||
auto loadVertexShaderFor(const ShaderProgramSpec& spec) -> bool;
|
||||
auto buildPipeline(SDL_GPUShader* fragment) -> SDL_GPUGraphicsPipeline*;
|
||||
[[nodiscard]] auto bestPresentMode() const -> SDL_GPUPresentMode;
|
||||
|
||||
SDL_Window* window_{nullptr};
|
||||
SDL_GPUDevice* device_{nullptr};
|
||||
SDL_GPUShader* vertex_shader_{nullptr};
|
||||
SDL_GPUShader* fragment_shader_{nullptr};
|
||||
SDL_GPUGraphicsPipeline* pipeline_{nullptr};
|
||||
|
||||
bool vsync_{true};
|
||||
std::string driver_name_;
|
||||
};
|
||||
|
||||
} // namespace Rendering
|
||||
41
source/rendering/sdl3gpu/shader_factory.hpp
Normal file
41
source/rendering/sdl3gpu/shader_factory.hpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace Rendering::Sdl3Gpu {
|
||||
|
||||
// Loads a compiled shader binary or source from disk and creates an SDL_GPUShader.
|
||||
// For SPIR-V: pass the .spv path with format = SDL_GPU_SHADERFORMAT_SPIRV.
|
||||
// For MSL: pass the .msl text path with format = SDL_GPU_SHADERFORMAT_MSL.
|
||||
inline auto loadShaderFromFile(SDL_GPUDevice* device,
|
||||
const std::filesystem::path& path,
|
||||
SDL_GPUShaderFormat format,
|
||||
const char* entrypoint,
|
||||
SDL_GPUShaderStage stage,
|
||||
Uint32 num_samplers,
|
||||
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
|
||||
std::size_t size = 0;
|
||||
void* data = SDL_LoadFile(path.string().c_str(), &size);
|
||||
if (data == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SDL_GPUShaderCreateInfo info{};
|
||||
info.code_size = size;
|
||||
info.code = static_cast<Uint8*>(data);
|
||||
info.entrypoint = entrypoint;
|
||||
info.format = format;
|
||||
info.stage = stage;
|
||||
info.num_samplers = num_samplers;
|
||||
info.num_storage_textures = 0;
|
||||
info.num_storage_buffers = 0;
|
||||
info.num_uniform_buffers = num_uniform_buffers;
|
||||
|
||||
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
|
||||
SDL_free(data);
|
||||
return shader;
|
||||
}
|
||||
|
||||
} // namespace Rendering::Sdl3Gpu
|
||||
99
source/rendering/shader_backend.cpp
Normal file
99
source/rendering/shader_backend.cpp
Normal file
@@ -0,0 +1,99 @@
|
||||
#include "rendering/shader_backend.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
namespace {
|
||||
|
||||
auto trimString(const std::string& str) -> std::string {
|
||||
const std::size_t start = str.find_first_not_of(" \t\r\n");
|
||||
const std::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 "";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto loadFileToString(const std::filesystem::path& path, std::string& out) -> bool {
|
||||
std::ifstream ifs(path, std::ios::in | std::ios::binary);
|
||||
if (!ifs) { return false; }
|
||||
std::ostringstream ss;
|
||||
ss << ifs.rdbuf();
|
||||
out = ss.str();
|
||||
return true;
|
||||
}
|
||||
|
||||
auto parseMetaFile(const std::filesystem::path& meta_path) -> ShaderMetadata {
|
||||
ShaderMetadata metadata;
|
||||
std::ifstream ifs(meta_path);
|
||||
if (!ifs) { return metadata; }
|
||||
|
||||
std::string line;
|
||||
while (std::getline(ifs, line)) {
|
||||
const std::size_t colon = line.find(':');
|
||||
if (colon == std::string::npos) { continue; }
|
||||
|
||||
std::string key = line.substr(0, colon);
|
||||
std::string value = trimString(line.substr(colon + 1));
|
||||
|
||||
std::transform(key.begin(), key.end(), key.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
key = trimString(key);
|
||||
|
||||
if (key == "name") { metadata.name = value; }
|
||||
else if (key == "author") { metadata.author = value; }
|
||||
else if (key == "ichannel0") { metadata.iChannel0 = value; }
|
||||
else if (key == "ichannel1") { metadata.iChannel1 = value; }
|
||||
else if (key == "ichannel2") { metadata.iChannel2 = value; }
|
||||
else if (key == "ichannel3") { metadata.iChannel3 = value; }
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
auto extractShaderMetadata(const std::string& source) -> ShaderMetadata {
|
||||
ShaderMetadata metadata;
|
||||
|
||||
std::istringstream stream(source);
|
||||
std::string line;
|
||||
int line_count = 0;
|
||||
constexpr int MAX_LINES_TO_CHECK = 30;
|
||||
|
||||
while (std::getline(stream, line) && line_count < MAX_LINES_TO_CHECK) {
|
||||
line_count++;
|
||||
|
||||
const std::size_t pos = line.find("//");
|
||||
if (pos == std::string::npos) { continue; }
|
||||
|
||||
const std::string comment = line.substr(pos + 2);
|
||||
std::string lower = comment;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
|
||||
auto valueAfterColon = [&]() {
|
||||
return trimString(comment.substr(comment.find(':') + 1));
|
||||
};
|
||||
|
||||
if (lower.find("name:") != std::string::npos) {
|
||||
metadata.name = valueAfterColon();
|
||||
} else if (lower.find("author:") != std::string::npos) {
|
||||
metadata.author = valueAfterColon();
|
||||
} else if (lower.find("ichannel0:") != std::string::npos) {
|
||||
metadata.iChannel0 = valueAfterColon();
|
||||
} else if (lower.find("ichannel1:") != std::string::npos) {
|
||||
metadata.iChannel1 = valueAfterColon();
|
||||
} else if (lower.find("ichannel2:") != std::string::npos) {
|
||||
metadata.iChannel2 = valueAfterColon();
|
||||
} else if (lower.find("ichannel3:") != std::string::npos) {
|
||||
metadata.iChannel3 = valueAfterColon();
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
} // namespace Rendering
|
||||
57
source/rendering/shader_backend.hpp
Normal file
57
source/rendering/shader_backend.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
struct ShaderMetadata {
|
||||
std::string name;
|
||||
std::string author;
|
||||
std::string iChannel0{"none"};
|
||||
std::string iChannel1{"none"};
|
||||
std::string iChannel2{"none"};
|
||||
std::string iChannel3{"none"};
|
||||
};
|
||||
|
||||
struct ShaderUniforms {
|
||||
float iTime{0.0f};
|
||||
float iResolutionX{0.0f};
|
||||
float iResolutionY{0.0f};
|
||||
};
|
||||
|
||||
struct ShaderProgramSpec {
|
||||
std::filesystem::path folder;
|
||||
std::string base_name;
|
||||
ShaderMetadata metadata;
|
||||
};
|
||||
|
||||
class IShaderBackend {
|
||||
public:
|
||||
IShaderBackend() = default;
|
||||
virtual ~IShaderBackend() = default;
|
||||
|
||||
IShaderBackend(const IShaderBackend&) = delete;
|
||||
IShaderBackend(IShaderBackend&&) = delete;
|
||||
auto operator=(const IShaderBackend&) -> IShaderBackend& = delete;
|
||||
auto operator=(IShaderBackend&&) -> IShaderBackend& = delete;
|
||||
|
||||
virtual auto init(SDL_Window* window) -> bool = 0;
|
||||
virtual auto loadShader(const ShaderProgramSpec& spec) -> bool = 0;
|
||||
virtual void render(const ShaderUniforms& uniforms) = 0;
|
||||
virtual void setVSync(bool vsync) = 0;
|
||||
virtual void cleanup() = 0;
|
||||
[[nodiscard]] virtual auto driverName() const -> std::string = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] auto makeOpenGLBackend() -> std::unique_ptr<IShaderBackend>;
|
||||
[[nodiscard]] auto makeSdl3GpuBackend() -> std::unique_ptr<IShaderBackend>;
|
||||
|
||||
[[nodiscard]] auto extractShaderMetadata(const std::string& source) -> ShaderMetadata;
|
||||
[[nodiscard]] auto loadFileToString(const std::filesystem::path& path, std::string& out) -> bool;
|
||||
[[nodiscard]] auto parseMetaFile(const std::filesystem::path& meta_path) -> ShaderMetadata;
|
||||
|
||||
} // namespace Rendering
|
||||
Reference in New Issue
Block a user