use anyhow::{Context, Result};
use par_term_emu_core_rust::cursor::CursorStyle;
use std::collections::BTreeMap;
use std::path::Path;
use std::time::Instant;
use wgpu::util::DeviceExt;
use wgpu::*;
mod builtin_textures;
mod cubemap;
mod cursor;
mod hot_reload;
pub mod pipeline;
mod state;
pub mod textures;
pub mod transpiler;
pub mod types;
mod uniforms;
use cubemap::CubemapTexture;
use pipeline::{
BindGroupInputs, create_bind_group, create_bind_group_layout, create_render_pipeline,
};
use textures::{ChannelTexture, load_channel_textures};
use transpiler::transpile_glsl_to_wgsl;
#[cfg_attr(not(debug_assertions), allow(dead_code))]
fn debug_shader_wgsl_filename(shader_name: &str) -> String {
crate::shader_debug::transpiled_wgsl_path(shader_name)
.to_string_lossy()
.into_owned()
}
fn write_debug_shader_wgsl(shader_name: &str, wgsl_source: &str) {
#[cfg(debug_assertions)]
{
let debug_filename = debug_shader_wgsl_filename(shader_name);
let _ = std::fs::remove_file(&debug_filename);
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let result = opts
.open(&debug_filename)
.and_then(|mut f| std::io::Write::write_all(&mut f, wgsl_source.as_bytes()));
match result {
Ok(()) => log::info!("Wrote debug shader to {}", debug_filename),
Err(e) => log::warn!("Failed to write debug shader: {}", e),
}
}
#[cfg(not(debug_assertions))]
{
let _ = (shader_name, wgsl_source);
}
}
fn animation_start_after_enabled_update(
currently_enabled: bool,
enabled: bool,
current_start: Instant,
now: Instant,
) -> Instant {
if enabled && !currently_enabled {
now
} else {
current_start
}
}
pub struct CustomShaderRenderer {
pub(crate) pipeline: RenderPipeline,
pub(crate) bind_group: BindGroup,
pub(crate) uniform_buffer: Buffer,
pub(crate) custom_uniform_buffer: Buffer,
pub(crate) intermediate_texture: Texture,
pub(crate) intermediate_texture_view: TextureView,
pub(crate) start_time: Instant,
pub(crate) animation_enabled: bool,
pub(crate) animation_speed: f32,
pub(crate) texture_width: u32,
pub(crate) texture_height: u32,
pub(crate) surface_format: TextureFormat,
pub(crate) bind_group_layout: BindGroupLayout,
pub(crate) sampler: Sampler,
pub(crate) scale_factor: f32,
pub(crate) window_opacity: f32,
pub(crate) keep_text_opaque: bool,
pub(crate) full_content_mode: bool,
pub(crate) brightness: f32,
pub(crate) auto_dim_under_text: bool,
pub(crate) auto_dim_strength: f32,
pub(crate) frame_count: u32,
pub(crate) last_frame_time: Instant,
pub(crate) mouse_position: [f32; 2],
pub(crate) mouse_click_position: [f32; 2],
pub(crate) mouse_button_down: bool,
pub(crate) frame_time_accumulator: f32,
pub(crate) frames_in_second: u32,
pub(crate) current_frame_rate: f32,
pub(crate) current_cursor_pos: (usize, usize),
pub(crate) previous_cursor_pos: (usize, usize),
pub(crate) current_cursor_color: [f32; 4],
pub(crate) previous_cursor_color: [f32; 4],
pub(crate) current_cursor_opacity: f32,
pub(crate) previous_cursor_opacity: f32,
pub(crate) cursor_change_time: f32,
pub(crate) current_cursor_style: CursorStyle,
pub(crate) previous_cursor_style: CursorStyle,
pub(crate) cursor_cell_width: f32,
pub(crate) cursor_cell_height: f32,
pub(crate) cursor_window_padding: f32,
pub(crate) cursor_content_offset_y: f32,
pub(crate) cursor_content_offset_x: f32,
pub(crate) cursor_shader_color: [f32; 4],
pub(crate) cursor_trail_duration: f32,
pub(crate) cursor_glow_radius: f32,
pub(crate) cursor_glow_intensity: f32,
pub(crate) key_press_time: f32,
pub(crate) channel_textures: [ChannelTexture; 4],
pub(crate) cubemap: CubemapTexture,
pub(crate) use_background_as_channel0: bool,
pub(crate) background_channel_texture: Option<ChannelTexture>,
pub(crate) background_channel0_blend_mode: par_term_config::ShaderBackgroundBlendMode,
pub(crate) background_color: [f32; 4],
pub(crate) progress_data: [f32; 4],
pub(crate) command_data: [f32; 4],
pub(crate) focused_pane: [f32; 4],
pub(crate) scroll_data: [f32; 4],
pub(crate) content_inset_right: f32,
pub(crate) custom_controls: Vec<par_term_config::ShaderControl>,
pub(crate) custom_uniform_values: BTreeMap<String, par_term_config::ShaderUniformValue>,
}
pub struct CustomShaderRendererConfig<'a> {
pub surface_format: TextureFormat,
pub shader_path: &'a Path,
pub width: u32,
pub height: u32,
pub animation_enabled: bool,
pub animation_speed: f32,
pub window_opacity: f32,
pub full_content_mode: bool,
pub channel_paths: &'a [Option<std::path::PathBuf>; 4],
pub cubemap_path: Option<&'a Path>,
pub custom_uniforms: &'a BTreeMap<String, par_term_config::ShaderUniformValue>,
pub background_channel0_blend_mode: par_term_config::ShaderBackgroundBlendMode,
}
impl CustomShaderRenderer {
pub fn new(
device: &Device,
queue: &Queue,
config: CustomShaderRendererConfig<'_>,
) -> Result<Self> {
let CustomShaderRendererConfig {
surface_format,
shader_path,
width,
height,
animation_enabled,
animation_speed,
window_opacity,
full_content_mode,
channel_paths,
cubemap_path,
custom_uniforms,
background_channel0_blend_mode,
} = config;
let glsl_source = std::fs::read_to_string(shader_path)
.with_context(|| format!("Failed to read shader file: {}", shader_path.display()))?;
let control_parse = par_term_config::parse_shader_controls(&glsl_source);
for warning in &control_parse.warnings {
log::warn!(
"Shader control warning line {}: {}",
warning.line,
warning.message
);
}
let custom_controls = control_parse.controls;
let custom_uniform_values = custom_uniforms.clone();
let wgsl_source = transpile_glsl_to_wgsl(&glsl_source, shader_path)?;
log::info!(
"Loaded custom shader from {} ({} bytes GLSL -> {} bytes WGSL)",
shader_path.display(),
glsl_source.len(),
wgsl_source.len()
);
log::debug!("Generated WGSL:\n{}", wgsl_source);
let shader_name = shader_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
write_debug_shader_wgsl(shader_name, &wgsl_source);
let module = naga::front::wgsl::parse_str(&wgsl_source)
.context("Custom shader WGSL parse failed")?;
let _info = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::empty(),
)
.validate(&module)
.context("Custom shader WGSL validation failed")?;
let shader_module = device.create_shader_module(ShaderModuleDescriptor {
label: Some("Custom Shader Module"),
source: ShaderSource::Wgsl(wgsl_source.clone().into()),
});
let (intermediate_texture, intermediate_texture_view) =
Self::create_intermediate_texture(device, surface_format, width, height);
let sampler = device.create_sampler(&SamplerDescriptor {
label: Some("Custom Shader Sampler"),
address_mode_u: AddressMode::ClampToEdge,
address_mode_v: AddressMode::ClampToEdge,
address_mode_w: AddressMode::ClampToEdge,
mag_filter: FilterMode::Nearest,
min_filter: FilterMode::Nearest,
mipmap_filter: MipmapFilterMode::Nearest,
..Default::default()
});
let channel_textures = load_channel_textures(device, queue, channel_paths);
let cubemap = match cubemap_path {
Some(path) => match CubemapTexture::from_prefix(device, queue, path) {
Ok(cm) => cm,
Err(e) => {
log::error!("Failed to load cubemap '{}': {}", path.display(), e);
CubemapTexture::placeholder(device, queue)
}
},
None => CubemapTexture::placeholder(device, queue),
};
let uniform_buffer = Self::create_uniform_buffer(device);
let custom_uniform_data =
crate::custom_shader_renderer::types::CustomShaderControlUniforms::from_controls(
&custom_controls,
&custom_uniform_values,
);
let custom_uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Custom Shader Control Uniform Buffer"),
contents: bytemuck::cast_slice(&[custom_uniform_data]),
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
});
let bind_group_layout = create_bind_group_layout(device);
let bind_group = create_bind_group(
device,
BindGroupInputs {
layout: &bind_group_layout,
uniform_buffer: &uniform_buffer,
intermediate_texture_view: &intermediate_texture_view,
custom_uniform_buffer: &custom_uniform_buffer,
sampler: &sampler,
channel_textures: &channel_textures,
cubemap: &cubemap,
},
);
let pipeline = create_render_pipeline(
device,
&shader_module,
&bind_group_layout,
surface_format,
Some("Custom Shader Pipeline"),
);
let now = Instant::now();
Ok(Self {
pipeline,
bind_group,
uniform_buffer,
custom_uniform_buffer,
intermediate_texture,
intermediate_texture_view,
start_time: now,
animation_enabled,
animation_speed,
texture_width: width,
texture_height: height,
surface_format,
bind_group_layout,
sampler,
window_opacity,
keep_text_opaque: false,
scale_factor: 1.0,
full_content_mode,
brightness: 1.0,
auto_dim_under_text: false,
auto_dim_strength: 0.35,
frame_count: 0,
last_frame_time: now,
mouse_position: [0.0, 0.0],
mouse_click_position: [0.0, 0.0],
mouse_button_down: false,
frame_time_accumulator: 0.0,
frames_in_second: 0,
current_frame_rate: 60.0,
current_cursor_pos: (0, 0),
previous_cursor_pos: (0, 0),
current_cursor_color: [1.0, 1.0, 1.0, 1.0],
previous_cursor_color: [1.0, 1.0, 1.0, 1.0],
current_cursor_opacity: 1.0,
previous_cursor_opacity: 1.0,
cursor_change_time: 0.0,
current_cursor_style: CursorStyle::SteadyBlock,
previous_cursor_style: CursorStyle::SteadyBlock,
cursor_cell_width: 10.0,
cursor_cell_height: 20.0,
cursor_window_padding: 0.0,
cursor_content_offset_y: 0.0,
cursor_content_offset_x: 0.0,
cursor_shader_color: [1.0, 1.0, 1.0, 1.0],
cursor_trail_duration: 0.5,
cursor_glow_radius: 80.0,
cursor_glow_intensity: 0.3,
key_press_time: 0.0,
channel_textures,
cubemap,
use_background_as_channel0: false,
background_channel_texture: None,
background_channel0_blend_mode,
background_color: [0.0, 0.0, 0.0, 0.0], progress_data: [0.0, 0.0, 0.0, 0.0],
command_data: [0.0, 0.0, 0.0, 0.0],
focused_pane: [0.0, 0.0, width as f32, height as f32],
scroll_data: [0.0, 0.0, 0.0, 0.0],
content_inset_right: 0.0,
custom_controls,
custom_uniform_values,
})
}
pub fn intermediate_texture_view(&self) -> &TextureView {
&self.intermediate_texture_view
}
pub fn render(
&mut self,
device: &Device,
queue: &Queue,
output_view: &TextureView,
apply_opacity: bool,
) -> Result<()> {
self.render_with_clear_color(
device,
queue,
output_view,
apply_opacity,
Color::TRANSPARENT,
)
}
pub fn render_with_clear_color(
&mut self,
device: &Device,
queue: &Queue,
output_view: &TextureView,
apply_opacity: bool,
clear_color: Color,
) -> Result<()> {
let now = Instant::now();
let time = if self.animation_enabled {
self.start_time.elapsed().as_secs_f32() * self.animation_speed.max(0.0)
} else {
0.0
};
let time_delta = now.duration_since(self.last_frame_time).as_secs_f32();
self.last_frame_time = now;
self.frame_time_accumulator += time_delta;
self.frames_in_second += 1;
if self.frame_time_accumulator >= 1.0 {
self.current_frame_rate = self.frames_in_second as f32 / self.frame_time_accumulator;
self.frame_time_accumulator = 0.0;
self.frames_in_second = 0;
}
self.frame_count = self.frame_count.wrapping_add(1);
let uniforms = self.build_uniforms(time, time_delta, apply_opacity);
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
let custom_uniforms =
crate::custom_shader_renderer::types::CustomShaderControlUniforms::from_controls(
&self.custom_controls,
&self.custom_uniform_values,
);
queue.write_buffer(
&self.custom_uniform_buffer,
0,
bytemuck::cast_slice(&[custom_uniforms]),
);
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("Custom Shader Encoder"),
});
{
let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("Custom Shader Render Pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: output_view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(clear_color),
store: StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.bind_group, &[]);
render_pass.draw(0..4, 0..1);
}
queue.submit(std::iter::once(encoder.finish()));
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn enabling_animation_when_already_enabled_preserves_start_time() {
let start_time = Instant::now() - Duration::from_secs(5);
let now = Instant::now();
assert_eq!(
animation_start_after_enabled_update(true, true, start_time, now),
start_time
);
}
#[test]
fn enabling_animation_from_disabled_starts_at_now() {
let start_time = Instant::now() - Duration::from_secs(5);
let now = Instant::now();
assert_eq!(
animation_start_after_enabled_update(false, true, start_time, now),
now
);
}
#[test]
fn debug_shader_wgsl_filename_matches_new_renderer_output_path() {
let path = debug_shader_wgsl_filename("matrix");
assert_eq!(
std::path::Path::new(&path),
crate::shader_debug::debug_dump_dir().join("par_term_matrix_shader.wgsl")
);
}
#[cfg(debug_assertions)]
#[test]
fn write_debug_shader_wgsl_refreshes_existing_output() {
let shader_name = format!("par_term_test_{}", std::process::id());
let path = debug_shader_wgsl_filename(&shader_name);
let _ = std::fs::remove_file(&path);
write_debug_shader_wgsl(&shader_name, "first");
write_debug_shader_wgsl(&shader_name, "second");
assert_eq!(
std::fs::read_to_string(&path).expect("read debug wgsl"),
"second"
);
std::fs::remove_file(&path).expect("remove debug wgsl");
}
#[cfg(all(debug_assertions, unix))]
#[test]
fn write_debug_shader_wgsl_creates_owner_only_file() {
use std::os::unix::fs::PermissionsExt;
let shader_name = format!("par_term_mode_{}", std::process::id());
let path = debug_shader_wgsl_filename(&shader_name);
let _ = std::fs::remove_file(&path);
write_debug_shader_wgsl(&shader_name, "secret");
let mode = std::fs::metadata(&path)
.expect("dump exists")
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o600, "dump must not be group/world readable");
std::fs::remove_file(&path).expect("remove debug wgsl");
}
#[cfg(all(debug_assertions, unix))]
#[test]
fn write_debug_shader_wgsl_does_not_follow_a_planted_symlink() {
let shader_name = format!("par_term_link_{}", std::process::id());
let path = debug_shader_wgsl_filename(&shader_name);
let target = crate::shader_debug::debug_dump_dir()
.join(format!("par_term_link_target_{}", std::process::id()));
std::fs::write(&target, "original").expect("seed symlink target");
let _ = std::fs::remove_file(&path);
std::os::unix::fs::symlink(&target, &path).expect("plant symlink");
write_debug_shader_wgsl(&shader_name, "attacker-visible");
assert_eq!(
std::fs::read_to_string(&target).expect("read symlink target"),
"original",
"the dump must not be written through the planted symlink"
);
let _ = std::fs::remove_file(&path);
std::fs::remove_file(&target).expect("remove symlink target");
}
}