From dd4d6f05ff875bd607c9e196b7da8b47c43bfc3b Mon Sep 17 00:00:00 2001 From: Eval Exec Date: Thu, 8 Oct 2026 10:13:52 +0800 Subject: [PATCH 1/3] refactor(runtime): derive frame demand metadata with strum --- .../src/render_thread/frame_sched.rs | 155 ++++++++++-------- 1 file changed, 88 insertions(+), 67 deletions(-) diff --git a/crates/neomacs-display-runtime/src/render_thread/frame_sched.rs b/crates/neomacs-display-runtime/src/render_thread/frame_sched.rs index 7d71b04d51..3d6ad4c1f0 100644 --- a/crates/neomacs-display-runtime/src/render_thread/frame_sched.rs +++ b/crates/neomacs-display-runtime/src/render_thread/frame_sched.rs @@ -121,102 +121,123 @@ pub(crate) enum Cadence { At(EventTime), } -/// Declares [`DemandReason`] and everything indexed by it from a single list. -/// The variant set, `ALL`, `COUNT` and `name` all come from these lines, so a -/// new reason is one line and cannot leave a hand-maintained table behind. -macro_rules! demand_reasons { - ($( - $(#[$variant_meta:meta])* - $variant:ident => $name:literal, - )+) => { - /// Why a frame is wanted. Diagnostic identity, not policy encoded as - /// strings. Deadline demands are keyed by this, so each reason holds at - /// most one scheduled deadline per window. - // Interface variants/fields defined by the scheduling plan; consumed as - // later stages migrate effects onto the coordinator. - #[allow(dead_code)] - #[derive( - Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, - enumset::EnumSetType, enum_map::Enum, - )] - #[enumset(no_super_impls, no_ops)] - pub(crate) enum DemandReason { - $($(#[$variant_meta])* $variant,)+ - } - - impl DemandReason { - /// Every reason, in declaration order. The order is the - /// counter/report order and matches the derived `Ord`. - pub(crate) const ALL: [DemandReason; Self::COUNT] = - [$(DemandReason::$variant,)+]; - - /// Number of reasons: the width of [`DemandReason::ALL`] and of - /// every per-reason counter array. - pub(crate) const COUNT: usize = [$(DemandReason::$variant,)+].len(); - - /// Stable snake_case name for diagnostics output. - pub(crate) const fn name(self) -> &'static str { - match self { - $(DemandReason::$variant => $name,)+ - } - } - } - }; -} - -demand_reasons! { - EditorCommit => "editor_commit", - CursorAnimation => "cursor_animation", +/// Why a frame is wanted. Diagnostic identity, not policy encoded as +/// strings. Deadline demands are keyed by this, so each reason holds at +/// most one scheduled deadline per window. +// Interface variants/fields defined by the scheduling plan; consumed as +// later stages migrate effects onto the coordinator. +#[allow(dead_code)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + enumset::EnumSetType, + enum_map::Enum, + strum::EnumCount, + strum::VariantArray, + strum::VariantNames, +)] +#[enumset(no_super_impls, no_ops)] +pub(crate) enum DemandReason { + #[strum(serialize = "editor_commit")] + EditorCommit, + #[strum(serialize = "cursor_animation")] + CursorAnimation, /// Infinite ambient compositor-only demand: the cursor color cycle /// (Stage 3 tracer bullet). Distinct from CursorAnimation so its MaxRate /// phase anchor cannot collide with the blink deadline. - CursorColorCycle => "cursor_color_cycle", - FiniteEffect => "finite_effect", - Transition => "transition", + #[strum(serialize = "cursor_color_cycle")] + CursorColorCycle, + #[strum(serialize = "finite_effect")] + FiniteEffect, + #[strum(serialize = "transition")] + Transition, /// Panes travelling between two layouts. Distinct from `Transition`, which /// is a cross-presentation content effect: a morph changes where the panes /// are drawn, so it needs frames even when the content is unchanged and /// nothing else on screen is moving. - PaneMotion => "pane_motion", + #[strum(serialize = "pane_motion")] + PaneMotion, /// A child frame travelling through its lifecycle: a popup fading in or /// out, drifting to a new anchor. Distinct from `PaneMotion` (which is /// whole-layout tiling morphs) so diagnostics can answer why a popup is /// still being redrawn after its parent's layout has settled. - ChildFrameMotion => "child_frame_motion", - Video => "video", - WebKit => "webkit", + #[strum(serialize = "child_frame_motion")] + ChildFrameMotion, + #[strum(serialize = "video")] + Video, + #[strum(serialize = "webkit")] + WebKit, /// Animated shader surfaces visible in a composited frame /// (docs/display-engine/SHADER_SURFACES.md). - ShaderSurface => "shader_surface", + #[strum(serialize = "shader_surface")] + ShaderSurface, /// Installed full-frame post shader whose time uniforms require a fresh /// composite even when the editor scene is unchanged. - FrameShader => "frame_shader", - Terminal => "terminal", - Expose => "expose", + #[strum(serialize = "frame_shader")] + FrameShader, + #[strum(serialize = "terminal")] + Terminal, + #[strum(serialize = "expose")] + Expose, /// A tick the coordinator did not ask for: the platform invalidated the /// surface (expose, resize, first map) or a runtime recovery path called /// request_redraw on the window directly. Distinct from Expose, which /// attributes the coordinator's own re-queue of work a present failed to /// deliver. - PlatformRedraw => "platform_redraw", - DebugCapture => "debug_capture", + #[strum(serialize = "platform_redraw")] + PlatformRedraw, + #[strum(serialize = "debug_capture")] + DebugCapture, /// New editor content or blink toggle needing a repaint. - Redisplay => "redisplay", + #[strum(serialize = "redisplay")] + Redisplay, /// Render-effect families (Stage 6). Each names the group animating so /// diagnostics can answer "why is this window still rendering?" without /// per-effect logging. - CursorEffect => "cursor_effect", - WindowEffect => "window_effect", - TextEffect => "text_effect", - ScrollEffect => "scroll_effect", - DecorativeEffect => "decorative_effect", - TransientEffect => "transient_effect", + #[strum(serialize = "cursor_effect")] + CursorEffect, + #[strum(serialize = "window_effect")] + WindowEffect, + #[strum(serialize = "text_effect")] + TextEffect, + #[strum(serialize = "scroll_effect")] + ScrollEffect, + #[strum(serialize = "decorative_effect")] + DecorativeEffect, + #[strum(serialize = "transient_effect")] + TransientEffect, } impl DemandReason { + /// Number of reasons: the width of every per-reason counter array. + pub(crate) const COUNT: usize = ::COUNT; + + /// Every reason, in declaration order. Preserve the fixed-size array API + /// while deriving the variant list from the enum. + pub(crate) const ALL: [Self; Self::COUNT] = { + let variants = ::VARIANTS; + let mut all = [variants[0]; Self::COUNT]; + let mut i = 0; + while i < Self::COUNT { + all[i] = variants[i]; + i += 1; + } + all + }; + + /// Stable diagnostic name, available in constant expressions. + pub(crate) const fn name(self) -> &'static str { + ::VARIANTS[self.index()] + } + /// Index into [`DemandReason::ALL`] / the per-reason counter arrays. The /// enum is fieldless with default discriminants, so the cast is the - /// declaration position, which is `ALL`'s order by construction; density is + /// declaration position, matching the derived variant/name order; density is /// pinned by `demand_reason_indices_are_dense`. pub(crate) const fn index(self) -> usize { self as usize From fe151babaa62db88658b25a8d5a762fd4f3fde8c Mon Sep 17 00:00:00 2001 From: Eval Exec Date: Thu, 8 Oct 2026 10:15:45 +0800 Subject: [PATCH 2/3] refactor(runtime): derive snake case demand names --- .../src/render_thread/frame_sched.rs | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/crates/neomacs-display-runtime/src/render_thread/frame_sched.rs b/crates/neomacs-display-runtime/src/render_thread/frame_sched.rs index 3d6ad4c1f0..51a5a6e86d 100644 --- a/crates/neomacs-display-runtime/src/render_thread/frame_sched.rs +++ b/crates/neomacs-display-runtime/src/render_thread/frame_sched.rs @@ -142,74 +142,54 @@ pub(crate) enum Cadence { strum::VariantNames, )] #[enumset(no_super_impls, no_ops)] +#[strum(serialize_all = "snake_case")] pub(crate) enum DemandReason { - #[strum(serialize = "editor_commit")] EditorCommit, - #[strum(serialize = "cursor_animation")] CursorAnimation, /// Infinite ambient compositor-only demand: the cursor color cycle /// (Stage 3 tracer bullet). Distinct from CursorAnimation so its MaxRate /// phase anchor cannot collide with the blink deadline. - #[strum(serialize = "cursor_color_cycle")] CursorColorCycle, - #[strum(serialize = "finite_effect")] FiniteEffect, - #[strum(serialize = "transition")] Transition, /// Panes travelling between two layouts. Distinct from `Transition`, which /// is a cross-presentation content effect: a morph changes where the panes /// are drawn, so it needs frames even when the content is unchanged and /// nothing else on screen is moving. - #[strum(serialize = "pane_motion")] PaneMotion, /// A child frame travelling through its lifecycle: a popup fading in or /// out, drifting to a new anchor. Distinct from `PaneMotion` (which is /// whole-layout tiling morphs) so diagnostics can answer why a popup is /// still being redrawn after its parent's layout has settled. - #[strum(serialize = "child_frame_motion")] ChildFrameMotion, - #[strum(serialize = "video")] Video, #[strum(serialize = "webkit")] WebKit, /// Animated shader surfaces visible in a composited frame /// (docs/display-engine/SHADER_SURFACES.md). - #[strum(serialize = "shader_surface")] ShaderSurface, /// Installed full-frame post shader whose time uniforms require a fresh /// composite even when the editor scene is unchanged. - #[strum(serialize = "frame_shader")] FrameShader, - #[strum(serialize = "terminal")] Terminal, - #[strum(serialize = "expose")] Expose, /// A tick the coordinator did not ask for: the platform invalidated the /// surface (expose, resize, first map) or a runtime recovery path called /// request_redraw on the window directly. Distinct from Expose, which /// attributes the coordinator's own re-queue of work a present failed to /// deliver. - #[strum(serialize = "platform_redraw")] PlatformRedraw, - #[strum(serialize = "debug_capture")] DebugCapture, /// New editor content or blink toggle needing a repaint. - #[strum(serialize = "redisplay")] Redisplay, /// Render-effect families (Stage 6). Each names the group animating so /// diagnostics can answer "why is this window still rendering?" without /// per-effect logging. - #[strum(serialize = "cursor_effect")] CursorEffect, - #[strum(serialize = "window_effect")] WindowEffect, - #[strum(serialize = "text_effect")] TextEffect, - #[strum(serialize = "scroll_effect")] ScrollEffect, - #[strum(serialize = "decorative_effect")] DecorativeEffect, - #[strum(serialize = "transient_effect")] TransientEffect, } From 8cba163401dd6501cd4fec844a1acc3cbb63be95 Mon Sep 17 00:00:00 2001 From: Eval Exec Date: Thu, 8 Oct 2026 10:16:38 +0800 Subject: [PATCH 3/3] refactor(runtime): use Webkit variant without name override --- .../neomacs-display-runtime/src/render_thread/frame_sched.rs | 3 +-- crates/neomacs-display-runtime/src/render_thread/lifecycle.rs | 2 +- .../src/render_thread/tests/frame_sched_test.rs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/neomacs-display-runtime/src/render_thread/frame_sched.rs b/crates/neomacs-display-runtime/src/render_thread/frame_sched.rs index 51a5a6e86d..07d8cebfe2 100644 --- a/crates/neomacs-display-runtime/src/render_thread/frame_sched.rs +++ b/crates/neomacs-display-runtime/src/render_thread/frame_sched.rs @@ -163,8 +163,7 @@ pub(crate) enum DemandReason { /// still being redrawn after its parent's layout has settled. ChildFrameMotion, Video, - #[strum(serialize = "webkit")] - WebKit, + Webkit, /// Animated shader surfaces visible in a composited frame /// (docs/display-engine/SHADER_SURFACES.md). ShaderSurface, diff --git a/crates/neomacs-display-runtime/src/render_thread/lifecycle.rs b/crates/neomacs-display-runtime/src/render_thread/lifecycle.rs index 17ac8d1687..a442fa7750 100644 --- a/crates/neomacs-display-runtime/src/render_thread/lifecycle.rs +++ b/crates/neomacs-display-runtime/src/render_thread/lifecycle.rs @@ -745,7 +745,7 @@ impl RenderApp { }; for (active, reason, rate) in [ - (webkit_active, DemandReason::WebKit, max_rate), + (webkit_active, DemandReason::Webkit, max_rate), (surfaces_active, DemandReason::ShaderSurface, surface_rate), ] { if active { diff --git a/crates/neomacs-display-runtime/src/render_thread/tests/frame_sched_test.rs b/crates/neomacs-display-runtime/src/render_thread/tests/frame_sched_test.rs index 0cbac03b06..257025e1f4 100644 --- a/crates/neomacs-display-runtime/src/render_thread/tests/frame_sched_test.rs +++ b/crates/neomacs-display-runtime/src/render_thread/tests/frame_sched_test.rs @@ -948,7 +948,7 @@ fn plan_attributes_the_frame_to_its_driving_reasons() { let plan = c.begin_frame(win(1), tick_at(now.plus(ms(1)))); assert!(plan.reasons.contains(DemandReason::CursorAnimation)); assert!(plan.reasons.contains(DemandReason::EditorCommit)); - assert!(!plan.reasons.contains(DemandReason::WebKit)); + assert!(!plan.reasons.contains(DemandReason::Webkit)); assert_eq!( plan.reasons.iter().collect::>(), vec![DemandReason::EditorCommit, DemandReason::CursorAnimation]