Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/neomacs-display-protocol/src/frame_glyphs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,8 @@ pub struct FrameGlyphBuffer {
pub outer_border_color: Color,
/// Background opacity (1.0 = opaque, 0.0 = transparent)
pub background_alpha: f32,
/// GNU whole-frame active/inactive opacity; negative means leave unchanged.
pub frame_alpha: [f32; 2],
/// Whether this frame should not accept keyboard focus
pub no_accept_focus: bool,

Expand Down Expand Up @@ -1565,6 +1567,7 @@ impl FrameGlyphBuffer {
outer_border_width: 0.0,
outer_border_color: Color::BLACK,
background_alpha: 1.0,
frame_alpha: [-1.0; 2],
no_accept_focus: false,
glyphs: Vec::with_capacity(10000),
frame_chrome: crate::frame_chrome::FrameChrome::default(),
Expand Down
10 changes: 10 additions & 0 deletions crates/neomacs-display-protocol/src/glyph_matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
//! `pixel_width`; GUI backends must use that rather than reconstructing every
//! glyph as one frame column.

fn default_frame_alpha() -> [f32; 2] {
[-1.0; 2]
}

use super::effect_config::EffectsConfig;
use super::face::{BoxVerticalEdges, FaceAttributes, UnderlineStyle};
use super::frame_chrome::{FrameChrome, FrameChromeContent, PresentationId};
Expand Down Expand Up @@ -2024,6 +2028,9 @@ pub struct FrameDisplayState {
#[serde(default)]
pub outer_border_color: Color,
pub background_alpha: f32,
/// GNU whole-frame active/inactive opacity; negative means leave unchanged.
#[serde(default = "default_frame_alpha")]
pub frame_alpha: [f32; 2],
pub no_accept_focus: bool,
pub window_infos: Vec<WindowInfo>,
pub transition_hints: Vec<ContentTransitionHint>,
Expand Down Expand Up @@ -2549,6 +2556,7 @@ impl FrameDisplayState {
a: 1.0,
},
background_alpha: 1.0,
frame_alpha: [-1.0; 2],
no_accept_focus: false,
window_infos: Vec::new(),
transition_hints: Vec::new(),
Expand Down Expand Up @@ -2622,6 +2630,7 @@ impl FrameDisplayState {
state.outer_border_width = buf.outer_border_width;
state.outer_border_color = buf.outer_border_color;
state.background_alpha = buf.background_alpha;
state.frame_alpha = buf.frame_alpha;
state.no_accept_focus = buf.no_accept_focus;
state.faces = buf.faces.clone();
state.font_catalog_generation = buf.font_catalog_generation;
Expand Down Expand Up @@ -2775,6 +2784,7 @@ impl FrameDisplayState {
buf.outer_border_width = self.outer_border_width;
buf.outer_border_color = self.outer_border_color;
buf.background_alpha = self.background_alpha;
buf.frame_alpha = self.frame_alpha;
buf.no_accept_focus = self.no_accept_focus;

// Copy faces
Expand Down
17 changes: 17 additions & 0 deletions crates/neomacs-display-protocol/src/glyph_matrix/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ fn settled_point(
}

use super::*;

#[test]
fn gnu_frame_alpha_survives_display_snapshot_materialization() {
let mut glyphs = FrameGlyphBuffer::with_size(640.0, 480.0);
glyphs.background_alpha = 0.25;
glyphs.frame_alpha = [0.8, -1.0];
let state = FrameDisplayState::from_frame_glyph_buffer(&glyphs);
assert_eq!(state.background_alpha, 0.25);
assert_eq!(state.frame_alpha, [0.8, -1.0]);
let materialized = state.materialize();
assert_eq!(materialized.background_alpha, 0.25);
assert_eq!(materialized.frame_alpha, [0.8, -1.0]);
assert_eq!(
FrameDisplayState::new(80, 25, 8.0, 16.0).frame_alpha,
[-1.0; 2]
);
}
use crate::DisplayFrameId;

mod row_appearance_test;
Expand Down
26 changes: 26 additions & 0 deletions crates/neomacs-display-runtime/src/render_thread/child_frames.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ impl EntryAnimation {
pub(crate) struct ChildFrameEntry {
pub frame_id: u64,
pub frame: FrameGlyphBuffer,
/// Last applied native opacity; GNU nil retains this across payloads.
pub applied_frame_alpha: f32,
/// Computed absolute position on screen (from parent_x/parent_y)
pub abs_x: f32,
pub abs_y: f32,
Expand Down Expand Up @@ -409,6 +411,25 @@ impl ChildFrameManager {
before != after
}

/// Reclaim time-expired composition ownership before mandatory admission.
/// Sampling expiry is pure: do not advance drift, placement or the submitted
/// interaction projection when a frame might still be refused.
pub fn reclaim_finished_composition(&mut self, sample: FrameSample) -> bool {
let mut changed = self.prune_crossfades(sample);
changed |= self.prune_dying(sample);
for entry in self.frames.values_mut() {
if entry
.animation
.as_ref()
.is_some_and(|animation| animation.motion.sample(sample).finished)
{
entry.animation = None;
changed = true;
}
}
changed
}

/// Drop every crossfade, returning their leases to the pool. The
/// device-loss path calls this: the leased textures died with the
/// device.
Expand Down Expand Up @@ -562,11 +583,16 @@ impl ChildFrameManager {
"child_frame_lifecycle: render_thread_child_buffer"
);

let applied_frame_alpha = self
.frames
.get(&frame_id.get())
.map_or(1.0, |entry| entry.applied_frame_alpha);
self.frames.insert(
frame_id.get(),
ChildFrameEntry {
frame_id: frame_id.get(),
frame: buf,
applied_frame_alpha,
abs_x: placed.root_relative().x(),
abs_y: placed.root_relative().y(),
clip_in_root: placed.clip_in_root(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,80 @@ impl LayoutDriver {
matches!(self, Self::Animating { .. })
}

/// Borrow-only eligibility for the same pane plan `on_frame` will place.
/// A pending first frame uses the ring's current picture, which becomes
/// previous on rotation; later ticks use only the picture already pinned.
/// Conservatively treat historical pixels as possibly fractional, without
/// allocating for enabled-but-idle motion or zero-weight terminal patches.
pub(in crate::render_thread) fn needs_native_conversion(
&self,
frame: FrameSample,
grid: PixelGrid,
candidate: Option<&SnapshotLease>,
) -> bool {
let Self::Animating { outgoing, .. } = self else {
return false;
};
let picture = match outgoing {
OutgoingPicture::Unpinned => candidate,
OutgoingPicture::Pinned(picture) => picture.as_ref(),
};
if picture.is_none() {
return false;
}
self.planned_sample(frame, grid).is_some_and(|sample| {
sample.pane_blits().iter().any(|pane| {
pane.source == neomacs_renderer_wgpu::PaneSource::Previous
&& pane.opacity > 0.0
&& pane.bounds.width > 0.0
&& pane.bounds.height > 0.0
})
})
}

/// The same borrow-only splice/sample used by eligibility and placement.
pub(in crate::render_thread) fn planned_sample(
&self,
frame: FrameSample,
grid: PixelGrid,
) -> Option<super::continuity::pane_layout::LayoutSample> {
let Self::Animating { morph, .. } = self else {
return None;
};
let spliced = morph.spliced(frame);
let morph = match spliced.as_ref() {
Some(spliced) => spliced,
None if morph.has_pending_retarget() => return None,
None => morph.as_ref(),
};
Some(morph.sample(frame, grid))
}

/// Retire only a terminal source. Keep the CPU morph for acquired-frame
/// settlement, and keep the pin decision so a later tick cannot repin history.
pub(in crate::render_thread) fn reclaim_finished_outgoing(
&mut self,
frame: FrameSample,
grid: PixelGrid,
) {
if self
.planned_sample(frame, grid)
.is_none_or(|sample| sample.motion.finished())
{
if let Self::Animating { outgoing, .. } = self {
*outgoing = OutgoingPicture::Pinned(None);
}
}
}

/// Geometry/device replacement invalidates GPU history, not the CPU morph
/// or its last submitted interaction. Do not repin a picture of the motion.
pub(in crate::render_thread) fn discard_outgoing_picture(&mut self) {
if let Self::Animating { outgoing, .. } = self {
*outgoing = OutgoingPicture::Pinned(None);
}
}

/// Pin the picture this motion fades *from*, if it has not been pinned.
///
/// Called by the render pass on every frame of a motion, with the ring's
Expand Down
44 changes: 26 additions & 18 deletions crates/neomacs-display-runtime/src/render_thread/frame_ingest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ impl RenderApp {
/// the instant swap GNU Emacs does.
fn lease_child_resize_crossfade(
render_policy: &crate::render_thread::render_quality::RenderQualityPolicy,
style: &crate::render_thread::state::ChildFrameStyle,
renderer: Option<&mut neomacs_renderer_wgpu::WgpuRenderer>,
window_state: &mut crate::render_thread::frame_windows::GuiFrameWindowState,
frame_id: u64,
Expand Down Expand Up @@ -193,33 +194,38 @@ impl RenderApp {
return None;
}
let scale = window_state.scale_factor() as f32;
let phys_w = (old_width * scale).ceil() as u32;
let phys_h = (old_height * scale).ceil() as u32;
let shadow_extent = if style.shadow_enabled {
style.shadow_layers as f32 * style.shadow_offset.max(0.0)
} else {
0.0
};
let phys_w = ((old_width + shadow_extent) * scale).ceil() as u32;
let phys_h = ((old_height + shadow_extent) * scale).ceil() as u32;
let Some(size) = neomacs_renderer_wgpu::SnapshotSize::new(phys_w, phys_h) else {
return None;
};
let renderer = renderer?;
let lease = renderer.acquire_snapshot(size).ok()?;
let view = lease.view().clone();
let atlas = window_state.render.compositor.glyph_atlas.as_mut()?;
renderer.render_frame_content(
&view,
let previous_scale = renderer.scale_factor();
renderer.set_scale_factor(scale);
renderer.capture_child_frame_picture(
&lease,
&old_entry.frame,
atlas,
phys_w,
phys_h,
0.0,
0.0,
false,
None,
0.0,
None,
None,
1.0,
1.0,
[0.0; 2],
style.corner_radius,
style.shadow_enabled,
style.shadow_layers,
style.shadow_offset,
style.shadow_opacity,
);
Some((lease, old_width, old_height, resize_spec))
renderer.set_scale_factor(previous_scale);
Some((
lease,
phys_w as f32 / scale,
phys_h as f32 / scale,
resize_spec,
))
}

#[cfg(feature = "webview")]
Expand Down Expand Up @@ -898,6 +904,7 @@ impl RenderApp {
let cursor_config = self.cursor_defaults.config_snapshot();
let resize_crossfade = Self::lease_child_resize_crossfade(
&self.render_policy,
&self.child_frame_style,
self.renderer.as_mut(),
window_state,
frame_id.get(),
Expand Down Expand Up @@ -986,6 +993,7 @@ impl RenderApp {
let new_presentation = frame.presentation_id;
let resize_crossfade = Self::lease_child_resize_crossfade(
&self.render_policy,
&self.child_frame_style,
self.renderer.as_mut(),
ws,
frame_id.get(),
Expand Down
50 changes: 50 additions & 0 deletions crates/neomacs-display-runtime/src/render_thread/frame_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,9 @@ pub(crate) struct GuiFrameRenderState {
/// into the swapchain as the final step. Recreated on resize.
pub(super) frame_post_src: Option<neomacs_renderer_wgpu::SnapshotLease>,
pub(super) native_content_src: Option<neomacs_renderer_wgpu::SnapshotLease>,
pub(super) child_opacity_src: Option<neomacs_renderer_wgpu::SnapshotLease>,
pub(super) child_resize_src: Option<neomacs_renderer_wgpu::SnapshotLease>,
pub(super) applied_frame_alpha: f32,
/// The current native input-method composition, if any.
///
/// `Option` is the active-state invariant: a preedit cannot be "active"
Expand Down Expand Up @@ -523,6 +526,9 @@ impl GuiFrameRenderState {
},
frame_post_src: None,
native_content_src: None,
child_opacity_src: None,
child_resize_src: None,
applied_frame_alpha: 1.0,
input_method: InputMethodState::default(),
cursor: CursorState::new(at),
mouse_pos: (0.0, 0.0),
Expand Down Expand Up @@ -552,6 +558,17 @@ impl GuiFrameRenderState {
}

pub(super) fn set_surface_state(&mut self, state: SurfaceState) {
if let GuiFramePresentState::Drawable { surface, .. } = self.present_state {
let compatible = matches!(state, SurfaceState::Drawable(next)
if surface.content_surface() == next.content_surface());
if !compatible {
// Historical pane UVs describe the old content geometry. Keep
// motion/interaction CPU state, but use the established missing-
// history fallback until an acquired frame publishes placement.
self.compositor.layout.discard_outgoing_picture();
self.mark_dirty();
}
}
self.present_state = match state {
SurfaceState::Suspended => GuiFramePresentState::Suspended,
SurfaceState::Drawable(surface) => GuiFramePresentState::Drawable {
Expand Down Expand Up @@ -595,6 +612,38 @@ impl GuiFrameRenderState {
Some((point.x(), point.y()))
}

/// Project already-applied native controls; scenes never replay setters.
pub(super) fn apply_frame_opacity(
&mut self,
controls: &crate::thread_comm::FrameOpacityState,
) -> bool {
let mut changed = false;
if let Some(alpha) = controls.applied(self.emacs_frame_id)
&& alpha != self.applied_frame_alpha
{
self.applied_frame_alpha = alpha;
changed = true;
}
let mut child_changed = false;
for (&id, entry) in &mut self.compositor.child_frames.frames {
if let Some(alpha) = controls.applied(id)
&& alpha != entry.applied_frame_alpha
{
entry.applied_frame_alpha = alpha;
child_changed = true;
}
}
if child_changed {
self.compositor.current_scene_generation =
crate::render_thread::frame_state::next_scene_generation();
self.compositor.current_row_damage = None;
}
if changed || child_changed {
self.compositor.dirty = true;
}
changed || child_changed
}

// Retained for focused render-state tests; production callers inspect the
// retained frame through narrower accessors.
#[allow(dead_code)]
Expand Down Expand Up @@ -2618,6 +2667,7 @@ impl GuiFrameWindowManager {
// Composition ring plus every running transition's leased
// source picture.
clear_frame_transition_textures(&mut render.compositor.transitions);
render.compositor.layout.discard_outgoing_picture();
// Full-frame post shader composition target.
render.frame_post_src = None;
render.native_content_src = None;
Expand Down
Loading
Loading