Repository navigation
Post-processing shaders (GLSL/WGSL, Ghostty-compatible) + CRT split-flap demo - #67
Merged
Merged
Conversation
Adds [shader] paths / animation to toastty-config, and two renderer-agnostic modules in the binary crate: shader_files (resolve ~ and config-relative paths, read sources, report per-file errors) and shader_watcher (watch each parent dir, flag + Wake on change). Not wired into main.rs yet.
…orKind notify makes relative watch paths absolute, so bare relative targets never matched. Two spellings of one directory share an inotify watch and notify reports events under the last-added one; watch and match against the canonical parent dir instead (file name kept as is). ShaderFileError now carries the io::ErrorKind so a watcher-triggered reload can skip NotFound during rename-based saves.
New crate::post module, not yet wired into the renderer: - compile: GLSL (Shadertoy/Ghostty dialect) and WGSL preludes, naga parse + validate, errors mapped back to lines in the user's file, GLSL -> WGSL for wgpu. - analysis: uniform-block members read by the entry point and its callees (on a compacted module), TOASTTY_EVENT_DURATION. - uniforms: one std140/WGSL-compatible block layout, checked against naga's offsets for both preludes. - state: shader clock, iMouse, focus, bells, cursor history, iDate, and the auto/always/never animation schedule. - PostChain: one fullscreen pass per shader, ping-pong textures, non-sRGB views (or copy fallback) so pass-through is bit-exact. Unit tests for the prelude, error mapping and analysis; GPU tests for pass-through, invert, multi-pass chains and uniform upload.
Renderer API: set_post_shaders (all-or-nothing, keeps the previous chain on failure), set_post_animation, set_focused, set_pointer, has_post_shaders. With no chain the frame path is unchanged. With a chain: the content pass always renders into scratch (which gains TEXTURE_BINDING and a non-sRGB view format; the surface gets a non-sRGB view format while a chain is on), the post passes replace the scratch->swapchain blit, and a UI pass draws the debug overlay, error banner, close dialog and chips unshaded on top. The IME preedit stays in the content. Frames with no damage but a due post tick or event run only the post and UI passes. Bells are read from Term in render_term; post deadlines join next_redraw_deadline. Also: text pipeline render_bg_range, color::linear_to_srgb, and an example (examples/post_shader.rs) to try shaders in a window.
…ointer to the renderer
…rrors - WGSL: the prelude now follows the user's file, so enable / requires / diagnostic directives work and user lines map one to one. - A missing or mistyped mainImage reports the signature it must have instead of naga's 'Unknown function'. - Error locations come from the narrowest span inside the user's file (pick_location), unit-tested.
… don't drop a shader change after a failed config reload
- Clamp TOASTTY_EVENT_DURATION to a day and use checked Instant math (a huge value used to panic). - A bell, cursor change or focus gain opens the duration window only if the chain reads that event's uniforms (all three when it reads none of them). - With a chain, a failed acquire (Outdated/Timeout/Occluded) no longer forces a full content rebuild; it counts as a dropped post frame, and time-driven frames slow to 4 Hz while occluded. - No post deadline while synchronized output pauses rendering. - iDate: `date +%z` runs on a helper thread, published via atomics, refreshed at the next local hour boundary; UTC until then or when date is missing (logged once at debug). - With a chain, the chips and the close dialog no longer force a full clear every frame (they're drawn in the UI pass).
…chains Renderer now draws into a Target: the window surface, or (cfg(test) only) an offscreen Bgra8UnormSrgb texture shaped like the swapchain. Renderer::headless builds one on the shared test device, so tests can drive the real render_term frame path. New render_tests: a pass-through chain matches the plain path for full and partial frames (direct and copy-fallback outputs); an inverting chain shades the content but not the error banner / debug overlay; an iMouse event runs a post-only frame with no damage; no post deadline while mode 2026 pauses rendering; a failed reload keeps the chain, and turning it off restores the plain frame pixel for pixel.
…nt frames - Only signature problems become the mainImage signature message: an unknown mainImage, or (WGSL) an argument-count/type or return-type error on the prelude's call. Other errors through the entry point (e.g. workgroupBarrier in a fragment shader) keep naga's message. - WGSL: a file ending inside a comment (prelude commented out, no entry point) or an unclosed block (parse error in the prelude) is reported at the last user line. - A post frame dropped by a timeout or an outdated surface keeps its pending event, so e.g. an iMouse press still gets its frame; only an occluded window drops it. - set_post_shaders starts the UTC-offset lookup when the chain reads iDate, so the first frames aren't in UTC.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds user post-process shaders (milestone M13). You run ShaderToy/Ghostty-style fragment shaders over the whole terminal, written in GLSL or WGSL, and they hot-reload. The spec is in
docs/milestones/m13-user-shaders.mdand the README has a new section.What's in it
mainImage). It uses the same uniform names and types as Ghostty'sshadertoy_prefix.glsl, so Ghostty shaders paste in unchanged.fn mainImage(fragCoord: vec2f) -> vec4f, with uniforms read astoastty.iTimeetc.).iTimeBell,iBellCursor,iBellCountandiBells[4], set from BEL. EachiBellsentry holds the cell position, the time of the bell and an exact age computed on the CPU.iCellSizeandiGridRect, for effects that work per cell.Termnow records each BEL and the cursor cell it rang at. A BEL that ends an OSC string doesn't count.const float TOASTTY_EVENT_DURATION = 1.5;to animate only for that long after an event it reads (bell, cursor change, focus gain).animation = "auto"stops continuous animation while unfocused and slows to 250 ms frames while occluded.file:line:col, at the right line in your file. The previous working chain keeps running. Shader files are watched and reload on save.examples/shaders/crt-splitflap.glsl. A static CRT look (curvature, scanlines, aperture mask, fringing, vignette). On each bell, a split-flap wave spreads out from the cursor: tiles riffle through characters taken from elsewhere on screen, then land back on their own.Visual evidence
Real app on Xvfb (1280×800, X11 path), default config plus
[shader] paths = ["…/crt-splitflap.glsl"].The split-flap wave after
printf '\a':Idle: the CRT look on its own (curvature, scanlines, vignette).
Mid-wave: the lighter tiles are flipping, the ones at the edge of the ring are folding over the hinge, and the text inside the ring has already settled.
A broken edit: the error banner names
broken2.glsl:196:14and is drawn unshaded (flat, not curved). The previous shader keeps running.CPU, measured from
/proc/<pid>/statwith the demo shader running:Tests
cargo test --workspace: all pass.toastty-rendertests:render_termtests on a new test-only offscreen target:[shader], shader file path resolution and loading, and the watcher. The watcher tests use real notify events, including symlinked directories and save-by-rename.TermBEL tests.Known limitations
alpha = 1makes a transparent window opaque. The demo keeps the input alpha.iChannel1+) or feedback buffers.