Skip to content

Commit

Permalink
feat: shader support with crt-easymode (#285)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukexor authored Jun 12, 2024
1 parent 80ef7b5 commit e5042ef
Show file tree
Hide file tree
Showing 8 changed files with 518 additions and 45 deletions.
22 changes: 15 additions & 7 deletions tetanes-core/src/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ use std::{
ops::{Deref, DerefMut},
sync::OnceLock,
};
use thiserror::Error;

#[derive(Error, Debug)]
#[must_use]
#[error("failed to parse `VideoFilter`")]
pub struct ParseVideoFilterError;

#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[must_use]
Expand All @@ -31,13 +37,15 @@ impl AsRef<str> for VideoFilter {
}
}

impl From<usize> for VideoFilter {
fn from(value: usize) -> Self {
if value == 1 {
Self::Ntsc
} else {
Self::Pixellate
}
impl TryFrom<usize> for VideoFilter {
type Error = ParseVideoFilterError;

fn try_from(value: usize) -> Result<Self, Self::Error> {
Ok(match value {
0 => Self::Pixellate,
1 => Self::Ntsc,
_ => return Err(ParseVideoFilterError),
})
}
}

Expand Down
143 changes: 143 additions & 0 deletions tetanes/shaders/crt-easymode.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Adapted from https://github.com/libretro/glsl-shaders/blob/master/crt/shaders/crt-easymode.glsl

var<private> vertices: array<vec2<f32>, 3> = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>(3.0, -1.0),
vec2<f32>(-1.0, 3.0),
);

// Vertex shader

struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) v_uv: vec2<f32>,
};

@vertex
fn vs_main(
@builtin(vertex_index) v_idx: u32
) -> VertexOutput {
var out: VertexOutput;
let vert = vertices[v_idx];
// Convert x from -1.0..1.0 to 0.0..1.0 and y from -1.0..1.0 to 1.0..0.0
out.v_uv = fma(vert, vec2(0.5, -0.5), vec2(0.5, 0.5));
out.position = vec4(vert, 0.0, 1.0);
return out;
}

// Fragment shader

@group(0) @binding(0) var<uniform> out_size: vec2<f32>;
@group(0) @binding(1) var tex: texture_2d<f32>;
@group(0) @binding(2) var tex_sampler: sampler;

const PI = 3.141592653589;

const SHARPNESS_H = 0.5;
const SHARPNESS_V = 1.0;
const MASK_STRENGTH = 0.3;
const MASK_DOT_WIDTH = 1.0;
const MASK_DOT_HEIGHT = 1.0;
const MASK_STAGGER = 0.0;
const MASK_SIZE = 1.0;
const SCANLINE_STRENGTH = 0.95;
const SCANLINE_BEAM_WIDTH_MIN = 2.5;
const SCANLINE_BEAM_WIDTH_MAX = 2.5;
const SCANLINE_BRIGHT_MIN = 0.3;
const SCANLINE_BRIGHT_MAX = 0.6;
const SCANLINE_CUTOFF = 400.0;
const GAMMA_INPUT = 1.0;
const GAMMA_OUTPUT = 2.2;
const BRIGHT_BOOST = 1.1;
const DILATION = 1.0;


// apply half-circle s-curve to distance for sharper (more pixelated) interpolation
fn curve_distance(x: f32, sharp: f32) -> f32 {
let x_step = step(0.5, x);
let curve = 0.5 - sqrt(0.25 - (x - x_step) * (x - x_step)) * sign(0.5 - x);

return mix(x, curve, sharp);
}

fn filter_lanczos(coeffs: vec4<f32>, color_matrix: mat4x4<f32>) -> vec3<f32> {
var col = color_matrix * coeffs;
let sample_min = min(color_matrix[1], color_matrix[2]);
let sample_max = max(color_matrix[1], color_matrix[2]);

col = clamp(col, sample_min, sample_max);

return col.rgb;
}

fn dilate(col: vec4<f32>) -> vec4<f32> {
let x = mix(vec4<f32>(1.0), col, DILATION);

return col * x;
}

fn tex2d(c: vec2<f32>) -> vec4<f32> {
return dilate(textureSample(tex, tex_sampler, c));
}

fn get_color_matrix(co: vec2<f32>, dx: vec2<f32>) -> mat4x4<f32> {
return mat4x4<f32>(tex2d(co - dx), tex2d(co), tex2d(co + dx), tex2d(co + 2.0 * dx));
}


@fragment
fn fs_main(@location(0) v_uv: vec2<f32>) -> @location(0) vec4<f32> {
let dims = vec2<f32>(textureDimensions(tex));
let inv_dims = 1.0 / dims;

let dx = vec2<f32>(inv_dims.x, 0.0);
let dy = vec2<f32>(0.0, inv_dims.y);
let pix_co = v_uv * dims - vec2<f32>(0.5, 0.5);
let tex_co = (floor(pix_co) + vec2<f32>(0.5, 0.5)) * inv_dims;
let dist = fract(pix_co);

var curve_x = curve_distance(dist.x, SHARPNESS_H * SHARPNESS_H);
var coeffs = PI * vec4<f32>(1.0 + curve_x, curve_x, 1.0 - curve_x, 2.0 - curve_x);

coeffs = max(abs(coeffs), vec4(1e-5));
coeffs = 2.0 * sin(coeffs) * sin(coeffs * 0.5) / (coeffs * coeffs);
coeffs /= dot(coeffs, vec4<f32>(1.0));

var col = filter_lanczos(coeffs, get_color_matrix(tex_co, dx));
var col2 = filter_lanczos(coeffs, get_color_matrix(tex_co + dy, dx));

col = mix(col, col2, curve_distance(dist.y, SHARPNESS_V));
col = pow(col, vec3<f32>(GAMMA_INPUT / (DILATION + 1.0)));

let luma = dot(vec3<f32>(0.2126, 0.7152, 0.0722), col);
let bright = (max(col.r, max(col.g, col.b)) + luma) * 0.5;
let scan_bright = clamp(bright, SCANLINE_BRIGHT_MIN, SCANLINE_BRIGHT_MAX);
let scan_beam = clamp(bright * SCANLINE_BEAM_WIDTH_MAX, SCANLINE_BEAM_WIDTH_MIN, SCANLINE_BEAM_WIDTH_MAX);
var scan_weight = 1.0 - pow(cos(v_uv.y * 2.0 * PI * dims.y) * 0.5 + 0.5, scan_beam) * SCANLINE_STRENGTH;

let insize = dims;
let mask = 1.0 - MASK_STRENGTH;
let mod_fac = floor(v_uv * out_size * dims / (insize * vec2<f32>(MASK_SIZE, MASK_DOT_HEIGHT * MASK_SIZE)));
let dot_no = i32(((mod_fac.x + (mod_fac.y % 2.0) * MASK_STAGGER) / MASK_DOT_WIDTH % 3.0));

var mask_weight: vec3<f32>;
if dot_no == 0 {
mask_weight = vec3<f32>(1.0, mask, mask);
} else if dot_no == 1 {
mask_weight = vec3<f32>(mask, 1.0, mask);
} else {
mask_weight = vec3<f32>(mask, mask, 1.0);
}

if insize.y >= SCANLINE_CUTOFF {
scan_weight = 1.0;
}

col2 = col.rgb;
col *= vec3<f32>(scan_weight);
col = mix(col, col2, scan_bright);
col *= mask_weight;
col = pow(col, vec3<f32>(1.0 / GAMMA_OUTPUT));

return vec4<f32>(col * BRIGHT_BOOST, 1.0);
}
7 changes: 6 additions & 1 deletion tetanes/src/nes/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::nes::input::{ActionBindings, Gamepads, Input};
use crate::nes::{
input::{ActionBindings, Gamepads, Input},
renderer::shader::Shader,
};
use anyhow::Context;
use egui::ahash::HashSet;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -92,6 +95,7 @@ pub struct RendererConfig {
pub show_menubar: bool,
pub embed_viewports: bool,
pub dark_theme: bool,
pub shader: Shader,
}

impl Default for RendererConfig {
Expand All @@ -112,6 +116,7 @@ impl Default for RendererConfig {
show_menubar: true,
embed_viewports: false,
dark_theme: true,
shader: Shader::default(),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion tetanes/src/nes/emulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ impl State {
ConfigEvent::ZapperConnected(connected) => {
self.control_deck.connect_zapper(*connected);
}
ConfigEvent::HideOverscan(_) | ConfigEvent::InputBindings => (),
ConfigEvent::HideOverscan(_) | ConfigEvent::InputBindings | ConfigEvent::Shader(_) => {}
}
}

Expand Down
6 changes: 5 additions & 1 deletion tetanes/src/nes/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use crate::{
config::Config,
emulation::FrameStats,
input::{AxisDirection, Gamepads, Input, InputBindings},
renderer::gui::{Menu, MessageType},
renderer::{
gui::{Menu, MessageType},
shader::Shader,
},
rom::RomData,
Nes, Running, State,
},
Expand Down Expand Up @@ -100,6 +103,7 @@ pub enum ConfigEvent {
RewindInterval(u32),
RunAhead(usize),
SaveSlot(u8),
Shader(Shader),
Speed(f32),
VideoFilter(VideoFilter),
ZapperConnected(bool),
Expand Down
105 changes: 72 additions & 33 deletions tetanes/src/nes/renderer.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use crate::{
nes::{
config::Config,
event::{EmulationEvent, NesEvent, RendererEvent, RunState, SendNesEvent, UiEvent},
event::{
ConfigEvent, EmulationEvent, NesEvent, RendererEvent, RunState, SendNesEvent, UiEvent,
},
input::Gamepads,
renderer::{
gui::{Gui, MessageType},
shader::Shader,
texture::Texture,
},
},
Expand Down Expand Up @@ -40,6 +43,7 @@ use winit::{
};

pub mod gui;
pub mod shader;
pub mod texture;

pub const OVERSCAN_TRIM: usize = (4 * Ppu::WIDTH * 8) as usize;
Expand Down Expand Up @@ -232,6 +236,16 @@ impl Renderer {
Some("nes frame"),
);

if !matches!(cfg.renderer.shader, Shader::None) {
let frame_painter =
shader::Resources::new(&render_state, &texture.view, cfg.renderer.shader);
render_state
.renderer
.write()
.callback_resources
.insert(frame_painter);
}

let gui = Rc::new(RefCell::new(Gui::new(
tx.clone(),
texture.sized_texture(),
Expand Down Expand Up @@ -406,46 +420,71 @@ impl Renderer {

self.gui.borrow_mut().on_event(event);

if let NesEvent::Renderer(event) = event {
match event {
RendererEvent::ViewportResized((viewport_width, _)) => {
// This expands the window width to the desired window width if the new viewport
// size allows
if let Some(window_size) = self.inner_size() {
let window_width = window_size.width as f32;
let desired_window_size = self.window_size(cfg);
let max_width = 0.8 * viewport_width;

if window_width < desired_window_size.x && window_width < max_width {
// We have room to resize up to desired_window_size
self.ctx.send_viewport_cmd_to(
ViewportId::ROOT,
ViewportCommand::InnerSize(desired_window_size),
);
match event {
NesEvent::Renderer(event) => {
match event {
RendererEvent::ViewportResized((viewport_width, _)) => {
// This expands the window width to the desired window width if the new viewport
// size allows
if let Some(window_size) = self.inner_size() {
let window_width = window_size.width as f32;
let desired_window_size = self.window_size(cfg);
let max_width = 0.8 * viewport_width;

if window_width < desired_window_size.x && window_width < max_width {
// We have room to resize up to desired_window_size
self.ctx.send_viewport_cmd_to(
ViewportId::ROOT,
ViewportCommand::InnerSize(desired_window_size),
);
}
}
}
}
RendererEvent::ToggleFullscreen => {
if platform::supports(platform::Feature::Viewports) {
self.ctx.set_embed_viewports(
cfg.renderer.fullscreen || cfg.renderer.embed_viewports,
RendererEvent::ToggleFullscreen => {
if platform::supports(platform::Feature::Viewports) {
self.ctx.set_embed_viewports(
cfg.renderer.fullscreen || cfg.renderer.embed_viewports,
);
}
self.ctx
.send_viewport_cmd_to(ViewportId::ROOT, ViewportCommand::Focus);
self.ctx.send_viewport_cmd_to(
ViewportId::ROOT,
ViewportCommand::Fullscreen(cfg.renderer.fullscreen),
);
}
self.ctx
.send_viewport_cmd_to(ViewportId::ROOT, ViewportCommand::Focus);
self.ctx.send_viewport_cmd_to(
ViewportId::ROOT,
ViewportCommand::Fullscreen(cfg.renderer.fullscreen),
);
RendererEvent::RomLoaded(_) => {
if self.state.borrow_mut().focused != Some(ViewportId::ROOT) {
self.ctx
.send_viewport_cmd_to(ViewportId::ROOT, ViewportCommand::Focus);
}
}
_ => (),
}
RendererEvent::RomLoaded(_) => {
if self.state.borrow_mut().focused != Some(ViewportId::ROOT) {
self.ctx
.send_viewport_cmd_to(ViewportId::ROOT, ViewportCommand::Focus);
}
NesEvent::Config(ConfigEvent::Shader(shader)) => {
if let Some(render_state) = &self.render_state {
if matches!(shader, Shader::None) {
render_state
.renderer
.write()
.callback_resources
.remove::<shader::Resources>();
} else {
let frame_painter = shader::Resources::new(
render_state,
&self.texture.view,
cfg.renderer.shader,
);
render_state
.renderer
.write()
.callback_resources
.insert(frame_painter);
}
}
_ => (),
}
_ => (),
}
}

Expand Down
Loading

0 comments on commit e5042ef

Please sign in to comment.