use crate::platform::shaders::SHADER_PRESETS;
use librashader::presets::ShaderPreset;
use librashader::presets::context::VideoDriver;
use librashader::runtime::gl::{FilterChain, FilterChainOptions, GLImage};
use librashader::runtime::{Size, Viewport};
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub struct ShaderManager {
filter_chain: Option<FilterChain>,
current_preset: Option<String>,
available_presets: Vec<PathBuf>,
current_index: usize,
frame_count: usize,
output_texture: Option<gl::types::GLuint>,
output_size: Option<Size<u32>>,
output_texture_owned_by_manager: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShaderOutputTexture {
gl_id: gl::types::GLuint,
}
impl ShaderOutputTexture {
pub fn gl_id(self) -> gl::types::GLuint {
self.gl_id
}
}
impl ShaderManager {
pub fn new(allowed_names: &[&str]) -> Self {
let available_presets = Self::discover_presets_filtered(allowed_names);
let current_index = Self::initial_current_index(&available_presets);
ShaderManager {
filter_chain: None,
current_preset: None,
available_presets,
current_index,
frame_count: 0,
output_texture: None,
output_size: None,
output_texture_owned_by_manager: true,
}
}
fn initial_current_index(presets: &[PathBuf]) -> usize {
presets
.iter()
.position(|p| p.file_name().and_then(|n| n.to_str()) == Some("stock.slangp"))
.unwrap_or(0)
}
fn ensure_output_texture(
&mut self,
width: u32,
height: u32,
) -> Result<gl::types::GLuint, String> {
if width == 0 || height == 0 {
return Err("Invalid output size".to_string());
}
let desired_size = Size::new(width, height);
let needs_realloc = self.output_size != Some(desired_size) || self.output_texture.is_none();
if needs_realloc {
let tex = unsafe {
let mut tex: gl::types::GLuint = 0;
gl::GenTextures(1, &mut tex);
if tex == 0 {
return Err("Failed to create output texture".to_string());
}
gl::BindTexture(gl::TEXTURE_2D, tex);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as i32);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as i32);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32);
gl::PixelStorei(gl::UNPACK_ALIGNMENT, 1);
gl::TexImage2D(
gl::TEXTURE_2D,
0,
gl::RGB8 as i32,
width as i32,
height as i32,
0,
gl::RGB,
gl::UNSIGNED_BYTE,
std::ptr::null(),
);
tex
};
if let Some(old) = self.output_texture.take()
&& self.output_texture_owned_by_manager
{
unsafe {
gl::DeleteTextures(1, &old);
}
}
self.output_texture = Some(tex);
self.output_size = Some(desired_size);
self.output_texture_owned_by_manager = true;
}
Ok(self.output_texture.expect("output texture must be set"))
}
fn discover_presets_filtered(allowed_names: &[&str]) -> Vec<PathBuf> {
Self::presets_for_names(allowed_names, SHADER_PRESETS)
.into_iter()
.filter(|p| p.exists())
.collect()
}
pub(crate) fn presets_for_names(
allowed_names: &[&str],
all_presets: &[(&str, &str)],
) -> Vec<PathBuf> {
allowed_names
.iter()
.filter_map(|allowed| {
all_presets
.iter()
.find(|(name, _)| name == allowed)
.map(|(_, path)| PathBuf::from(path))
})
.collect()
}
pub fn load_preset(
&mut self,
preset_path: &Path,
gl_context: Arc<glow::Context>,
) -> Result<(), String> {
let preset = ShaderPreset::try_parse_with_driver_context(preset_path, VideoDriver::GlCore)
.map_err(|e| format!("Failed to parse shader preset: {}", e))?;
let options = FilterChainOptions {
glsl_version: 0, use_dsa: false, force_no_mipmaps: false,
disable_cache: false,
};
let filter_chain = unsafe {
FilterChain::load_from_preset(preset, gl_context, Some(&options))
.map_err(|e| format!("Failed to load filter chain: {}", e))?
};
self.filter_chain = Some(filter_chain);
self.current_preset = Some(preset_path.to_string_lossy().to_string());
self.sync_current_index(preset_path);
self.frame_count = 0;
Ok(())
}
pub fn apply_shader(
&mut self,
input_texture: gl::types::GLuint,
input_width: u32,
input_height: u32,
viewport_width: u32,
viewport_height: u32,
) -> Result<(), String> {
if self.filter_chain.is_none() {
return Ok(());
}
let output_texture = self.ensure_output_texture(viewport_width, viewport_height)?;
let filter_chain = self
.filter_chain
.as_mut()
.expect("filter_chain must be present");
let input_size = Size::new(input_width, input_height);
let image = GLImage {
handle: Some(glow::NativeTexture(
std::num::NonZero::new(input_texture).ok_or("Invalid texture ID")?,
)),
format: gl::RGB8,
size: input_size,
};
let output_image = GLImage {
handle: Some(glow::NativeTexture(
std::num::NonZero::new(output_texture).ok_or("Invalid output texture ID")?,
)),
format: gl::RGB8,
size: Size::new(viewport_width, viewport_height),
};
let viewport = Viewport {
x: 0.0,
y: 0.0,
size: Size::new(viewport_width, viewport_height),
output: &output_image,
mvp: None,
};
unsafe {
filter_chain
.frame(
&image,
&viewport,
self.frame_count,
None, )
.map_err(|e| format!("Failed to apply shader: {}", e))?;
}
self.frame_count = self.frame_count.wrapping_add(1);
Ok(())
}
pub fn output_texture(&self) -> Option<gl::types::GLuint> {
self.output_texture
}
pub fn take_output_texture_ownership(
&mut self,
texture: gl::types::GLuint,
) -> Option<ShaderOutputTexture> {
if self.output_texture == Some(texture) {
self.output_texture_owned_by_manager = false;
Some(ShaderOutputTexture { gl_id: texture })
} else {
None
}
}
pub fn cycle_shader(&mut self, gl_context: Arc<glow::Context>) -> Result<(), String> {
if self.available_presets.is_empty() {
return Err("No shader presets available".to_string());
}
self.current_index = (self.current_index + 1) % self.available_presets.len();
let preset_path = self.available_presets[self.current_index].clone();
self.load_preset(&preset_path, gl_context)?;
Ok(())
}
pub fn current_preset_name(&self) -> Option<&str> {
self.current_preset.as_deref()
}
pub fn has_shader(&self) -> bool {
self.filter_chain.is_some()
}
fn sync_current_index(&mut self, preset_path: &Path) {
if let Some(idx) = self.available_presets.iter().position(|p| p == preset_path) {
self.current_index = idx;
}
}
}
impl Drop for ShaderManager {
fn drop(&mut self) {
if let Some(tex) = self.output_texture.take()
&& self.output_texture_owned_by_manager
{
unsafe {
gl::DeleteTextures(1, &tex);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gba::console::config::GBA_FILTER_NAMES;
impl ShaderManager {
fn with_presets(presets: Vec<PathBuf>) -> Self {
let current_index = ShaderManager::initial_current_index(&presets);
ShaderManager {
filter_chain: None,
current_preset: None,
available_presets: presets,
current_index,
frame_count: 0,
output_texture: None,
output_size: None,
output_texture_owned_by_manager: true,
}
}
fn with_output_texture(texture: gl::types::GLuint, owned_by_manager: bool) -> Self {
let mut manager = ShaderManager::with_presets(Vec::new());
manager.output_texture = Some(texture);
manager.output_texture_owned_by_manager = owned_by_manager;
manager
}
}
#[test]
fn test_cycle_starts_after_initially_loaded_preset() {
let presets = vec![
PathBuf::from("shaders/stock.slangp"),
PathBuf::from("vendor/slang-shaders/crt/crt-lottes.slangp"),
PathBuf::from(
"vendor/slang-shaders/edge-smoothing/xbrz/xbrz-freescale-multipass.slangp",
),
PathBuf::from("vendor/slang-shaders/ntsc/ntsc-256px-composite.slangp"),
];
let mut mgr = ShaderManager::with_presets(presets);
mgr.sync_current_index(Path::new("shaders/stock.slangp"));
let next_index = (mgr.current_index + 1) % mgr.available_presets.len();
assert_eq!(
mgr.available_presets[next_index],
PathBuf::from("vendor/slang-shaders/crt/crt-lottes.slangp")
);
}
#[test]
fn test_initial_cycle_without_loaded_shader_starts_after_stock() {
let presets = vec![
PathBuf::from("shaders/stock.slangp"), PathBuf::from("vendor/slang-shaders/crt/crt-lottes.slangp"), PathBuf::from(
"vendor/slang-shaders/edge-smoothing/xbrz/xbrz-freescale-multipass.slangp",
), PathBuf::from("vendor/slang-shaders/ntsc/ntsc-256px-composite.slangp"), ];
let mgr = ShaderManager::with_presets(presets);
let next_index = (mgr.current_index + 1) % mgr.available_presets.len();
assert_eq!(
mgr.available_presets[next_index],
PathBuf::from("vendor/slang-shaders/crt/crt-lottes.slangp"),
"first F4 from 'no shader' state should land on crt, not index {}",
next_index
);
}
#[test]
fn take_output_texture_ownership_returns_current_texture_and_marks_external() {
let mut manager = ShaderManager::with_output_texture(7, true);
let texture = manager.take_output_texture_ownership(7);
let owned_by_manager = manager.output_texture_owned_by_manager;
manager.output_texture = None;
assert_eq!(texture.map(ShaderOutputTexture::gl_id), Some(7));
assert!(!owned_by_manager);
}
#[test]
fn take_output_texture_ownership_rejects_non_current_texture() {
let mut manager = ShaderManager::with_output_texture(7, true);
let texture = manager.take_output_texture_ownership(8);
let owned_by_manager = manager.output_texture_owned_by_manager;
manager.output_texture = None;
assert_eq!(texture, None);
assert!(owned_by_manager);
}
#[test]
fn test_presets_for_names_returns_only_allowed_presets() {
let all: &[(&str, &str)] = &[
("none", "shaders/stock.slangp"),
("crt", "vendor/slang-shaders/crt/crt-lottes.slangp"),
(
"smooth",
"vendor/slang-shaders/edge-smoothing/xbrz/xbrz-freescale-multipass.slangp",
),
(
"ntsc",
"vendor/slang-shaders/ntsc/ntsc-256px-composite.slangp",
),
(
"pal",
"vendor/slang-shaders/pal/decoupled-guest-advanced-pal 3-RF.slangp",
),
("dmg", "vendor/slang-shaders/handheld/gameboy.slangp"),
];
let gb_allowed = &["none", "dmg"];
let gb_presets = ShaderManager::presets_for_names(gb_allowed, all);
assert_eq!(gb_presets.len(), 2, "GB should have exactly 2 presets");
assert!(gb_presets.contains(&PathBuf::from("shaders/stock.slangp")));
assert!(gb_presets.contains(&PathBuf::from(
"vendor/slang-shaders/handheld/gameboy.slangp"
)));
assert!(
!gb_presets
.iter()
.any(|p| p.to_str().unwrap_or("").contains("crt")),
"GB must not include crt"
);
let nes_allowed = &["none", "crt", "smooth", "ntsc", "pal"];
let nes_presets = ShaderManager::presets_for_names(nes_allowed, all);
assert_eq!(nes_presets.len(), 5, "NES should have exactly 5 presets");
assert!(
!nes_presets
.iter()
.any(|p| p.to_str().unwrap_or("").contains("gameboy")),
"NES must not include dmg"
);
}
#[test]
fn test_presets_for_names_preserves_allowed_names_order() {
let all: &[(&str, &str)] = &[
(
"gba-lcd-grid",
"vendor/slang-shaders/handheld/console-border/gba-lcd-grid-v2.slangp",
),
(
"sp101-color",
"vendor/slang-shaders/handheld/color-mod/sp101-color.slangp",
),
("none", "shaders/stock.slangp"),
(
"nso-gba-color",
"vendor/slang-shaders/handheld/color-mod/NSO-gba-color.slangp",
),
("gba-lcd", "shaders/gba-lcd.slangp"),
("agb001", "vendor/slang-shaders/handheld/agb001.slangp"),
];
let discovered = ShaderManager::presets_for_names(GBA_FILTER_NAMES, all);
let expected = vec![
PathBuf::from("shaders/stock.slangp"),
PathBuf::from("shaders/gba-lcd.slangp"),
PathBuf::from("vendor/slang-shaders/handheld/agb001.slangp"),
PathBuf::from("vendor/slang-shaders/handheld/color-mod/NSO-gba-color.slangp"),
PathBuf::from("vendor/slang-shaders/handheld/color-mod/sp101-color.slangp"),
PathBuf::from("vendor/slang-shaders/handheld/console-border/gba-lcd-grid-v2.slangp"),
];
assert_eq!(discovered, expected);
}
}