Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion apps/notes/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1850,7 +1850,7 @@ mod tests {
let page_ref = nav.view_by_id(&mut cx, page);
page_ref
.as_stack_navigation_view()
.show_at_rest(&mut cx, size.x);
.show_at_rest(&mut cx);
let actions = cx.capture_actions(|cx| {
cx.widget_action(
page_ref.widget_uid(),
Expand Down
820 changes: 670 additions & 150 deletions draw/src/image_cache.rs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions draw/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ pub use crate::{
draw_list_2d::{DrawList2d, DrawListExt, ManyInstances, Redrawing, RedrawingApi},
image_cache::{
decode_image_from_data, evict_image_from_cache, handle_image_cache_network_responses,
image_size_by_data,
has_enough_pixels, image_size_by_data, is_animated_image, is_decoding_image,
looks_like_svg, load_image_file_by_path_async, load_image_from_cache,
load_image_from_data_async,
load_image_from_data_async, load_image_from_data_async_at_size,
load_image_http_by_url_async, process_async_image_load, AsyncImageLoad, AsyncLoadResult,
ImageBuffer, ImageCache, ImageCacheImpl, ImageError, JpgDecodeErrors, PngDecodeErrors,
},
Expand Down
4 changes: 2 additions & 2 deletions draw/src/turtle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ pub enum AlignEntry {
}

/// Specifies how a turtle should walk.
#[derive(Copy, Clone, Default, Debug, Script, ScriptHook)]
#[derive(Copy, Clone, Default, Debug, PartialEq, Script, ScriptHook)]
pub struct Walk {
#[doc(hidden)]
#[live]
Expand Down Expand Up @@ -254,7 +254,7 @@ impl Walk {
}
}

#[derive(Copy, Clone, Debug, Script, ScriptHook)]
#[derive(Copy, Clone, Debug, PartialEq, Script, ScriptHook)]
pub struct Metrics {
#[live]
pub descender: f64,
Expand Down
7 changes: 7 additions & 0 deletions platform/src/cx_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1530,6 +1530,13 @@ impl Cx {
pub fn text_ime_was_dismissed(&mut self) {
self.publish_hosted_ime(crate::ime::HostedImeState::default());
self.keyboard.set_text_ime_dismissed();
// A focused TextInput re-pushes ShowTextIME on every draw, so a show op
// queued by a frame drawn just before this dismissal landed can still be
// in platform_ops. If it drains after the backend forgets its last-shown
// config (e.g. Android's ResizeTextIME-closed clearing last_ime_config),
// it re-opens the keyboard the user just closed. Drop the stale shows.
self.platform_ops
.retain(|op| !matches!(op, CxOsOp::ShowTextIME(..)));
self.platform_ops.push_back(CxOsOp::HideTextIME);
}

Expand Down
91 changes: 71 additions & 20 deletions platform/src/draw_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,35 @@ impl ScriptHook for DrawVars {
}

impl DrawVars {
/// Instance slots can carry packed integer bits, including NaN payloads.
/// Preserve signed zero and bit patterns while avoiding identical GPU writes.
fn copy_changed_slots(
target: &mut [f32],
value: &[f32],
perf: &mut crate::perf_monitor::PerfMonitor,
) -> bool {
debug_assert_eq!(target.len(), value.len());
let changed = !target
.iter()
.zip(value)
.all(|(a, b)| a.to_bits() == b.to_bits());
if perf.enabled() {
perf.count_work(
if changed {
"shader.write.changed"
} else {
"shader.write.unchanged"
},
"DrawVars",
1,
);
}
if changed {
target.copy_from_slice(value);
}
changed
}

/// Loud report for a failed draw-shader compile. A shader that fails
/// must never fall back silently: the draw is skipped from then on and
/// the only on-screen symptom is a flat clear-color region where the
Expand Down Expand Up @@ -243,12 +272,13 @@ impl DrawVars {
let key: ScriptValue = input.id.into();
if obj_map.contains_key(&key) {
for j in 0..repeat {
for i in 0..input.slots {
instances[input.offset + i + j * stride] =
inst_slice[input.offset + i]
}
let offset = input.offset + j * stride;
any_updated |= Self::copy_changed_slots(
&mut instances[offset..offset + input.slots],
&inst_slice[input.offset..input.offset + input.slots],
&mut cx.perf_monitor,
);
}
any_updated = true;
}
}

Expand Down Expand Up @@ -288,11 +318,12 @@ impl DrawVars {
for input in &sh.mapping.dyn_uniforms.inputs {
let key: ScriptValue = input.id.into();
if obj_map.contains_key(&key) {
for i in 0..input.slots {
draw_call.dyn_uniforms[input.offset + i] =
self.dyn_uniforms[input.offset + i]
}
any_updated = true;
let range = input.offset..input.offset + input.slots;
any_updated |= Self::copy_changed_slots(
&mut draw_call.dyn_uniforms[range.clone()],
&self.dyn_uniforms[range],
&mut cx.perf_monitor,
);
}
}

Expand Down Expand Up @@ -525,8 +556,12 @@ impl DrawVars {
return;
};

for i in 0..slots {
draw_call.dyn_uniforms[offset + i] = value[i];
if !Self::copy_changed_slots(
&mut draw_call.dyn_uniforms[offset..offset + slots],
&value[..slots],
&mut cx.perf_monitor,
) {
return;
}
draw_call.mark_uniforms_dirty(uniforms_gen);
if let Some(pass_id) = draw_list.draw_pass_id {
Expand Down Expand Up @@ -570,7 +605,14 @@ impl DrawVars {
if draw_call.draw_shader_id != draw_shader_id {
return false;
}
draw_call.dyn_uniforms[offset..offset + slots].copy_from_slice(&value[..slots]);
if !Self::copy_changed_slots(
&mut draw_call.dyn_uniforms[offset..offset + slots],
&value[..slots],
&mut cx.perf_monitor,
) {
// Success means a valid target, including an already-current value.
return true;
}
draw_call.mark_uniforms_dirty(uniforms_gen);
if let Some(pass_id) = draw_list.draw_pass_id {
cx.passes[pass_id].paint_dirty = true;
Expand Down Expand Up @@ -677,8 +719,12 @@ impl DrawVars {
if draw_call.draw_shader_id != draw_shader_id {
continue;
}
for i in 0..slots {
draw_call.dyn_uniforms[offset + i] = value[i];
if !Self::copy_changed_slots(
&mut draw_call.dyn_uniforms[offset..offset + slots],
&value[..slots],
&mut cx.perf_monitor,
) {
continue;
}
draw_call.mark_uniforms_dirty(Cx::next_uniform_gen_from(uniform_gen));
touched = true;
Expand Down Expand Up @@ -728,13 +774,18 @@ impl DrawVars {

let instances = &mut all_instances[inst.instance_offset..];

// Update all instances in this area
let mut changed = false;
for j in 0..inst.instance_count {
for i in 0..slots {
instances[input.offset + i + j * stride] = value[i];
}
let offset = input.offset + j * stride;
changed |= Self::copy_changed_slots(
&mut instances[offset..offset + slots],
&value[..slots],
&mut cx.perf_monitor,
);
}
if !changed {
return;
}

draw_call.instance_dirty = true;
if let Some(pass_id) = draw_list.draw_pass_id {
cx.passes[pass_id].paint_dirty = true;
Expand Down
2 changes: 1 addition & 1 deletion platform/src/event/finger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ pub struct TouchUpdateEvent {

/// Inset represents spacing values for all four edges (left, top, right, bottom).
/// Used for both margin (outer spacing) and padding (inner spacing).
#[derive(Clone, Copy, Default, Debug, Script)]
#[derive(Clone, Copy, Default, Debug, PartialEq, Script)]
pub struct Inset {
/// The left inset.
#[live]
Expand Down
87 changes: 67 additions & 20 deletions platform/src/os/linux/android/android.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,15 @@ impl Cx {
// but immediately torn down again (rotation race).
self.os.needs_first_draw = true;
}
// A hide still pending after this draw really hides the keyboard,
// unless the draw queued a `ShowTextIME` for an input that took focus.
if self.os.pending_ime_hide
&& !self.platform_ops.iter().any(|op| matches!(op, CxOsOp::ShowTextIME(..)))
{
self.os.pending_ime_hide = false;
unsafe { android_jni::to_java_show_keyboard(false); }
self.os.last_ime_config = None;
}
}
Ok(message) => {
self.handle_message(message);
Expand Down Expand Up @@ -1786,6 +1795,7 @@ impl Cx {
display.surface,
);
if swapped != 0 {
self.perf_monitor.frame_boundary(Cx::monotonic_now());
self.hide_android_surface_cover_after_first_present_if_needed();
self.request_android_surface_snapshot_refresh_after_present_if_needed();
}
Expand Down Expand Up @@ -1813,6 +1823,7 @@ impl Cx {
self.perf_monitor.add(crate::perf_monitor::PERF_CHANNEL_DRAWABLE_WAIT, us);
}
if swapped != 0 {
self.perf_monitor.frame_boundary(Cx::monotonic_now());
self.hide_android_surface_cover_after_first_present_if_needed();
self.request_android_surface_snapshot_refresh_after_present_if_needed();
}
Expand Down Expand Up @@ -2593,8 +2604,17 @@ impl Cx {
if self.windows[window_id].is_popup {
continue;
}
let start = self.seconds_since_app_start();
let metrics = self.collect_gpu_pass_metrics(*draw_pass_id);
// Collecting Studio statistics walks the full draw tree and
// allocates geometry/texture inventories. Normal rendering
// has no consumer for this work; local captures still opt in.
let profile = (Cx::has_studio_web_socket()
|| Cx::local_profile_capture_enabled())
.then(|| {
(
self.seconds_since_app_start(),
self.collect_gpu_pass_metrics(*draw_pass_id),
)
});
self.draw_pass_to_window_for_active_backend(*draw_pass_id);

// Draw popup window passes as overlays on the same surface
Expand All @@ -2611,18 +2631,38 @@ impl Cx {
}
}

let end = self.seconds_since_app_start();
Cx::send_studio_message(AppToStudio::GPUSample(GPUSample {
start,
end,
draw_calls: metrics.draw_calls,
instances: metrics.instances,
vertices: metrics.vertices,
instance_bytes: metrics.instance_bytes,
uniform_bytes: metrics.uniform_bytes,
vertex_buffer_bytes: metrics.vertex_buffer_bytes,
texture_bytes: metrics.texture_bytes,
}));
if let Some((start, metrics)) = profile {
let end = self.seconds_since_app_start();
Cx::send_studio_message(AppToStudio::GPUSample(GPUSample {
start,
end,
draw_calls: metrics.draw_calls,
instances: metrics.instances,
vertices: metrics.vertices,
instance_bytes: metrics.instance_bytes,
uniform_bytes: metrics.uniform_bytes,
vertex_buffer_bytes: metrics.vertex_buffer_bytes,
texture_bytes: metrics.texture_bytes,
}));
}
// Android has its own GL window path, so it must also drain
// explicit app-frame captures before swapping the surface.
#[cfg(not(use_vulkan))]
if self.os.has_drawable_surface() {
self.opengl_capture_window(
Some(window_id.id()),
self.os.display_size.x as u32,
self.os.display_size.y as u32,
);
}
#[cfg(use_vulkan)]
if self.os.vulkan.is_none() && self.os.has_drawable_surface() {
self.opengl_capture_window(
Some(window_id.id()),
self.os.display_size.x as u32,
self.os.display_size.y as u32,
);
}
self.present_window_for_active_backend();
}
CxDrawPassParent::DrawPass(_) => {
Expand Down Expand Up @@ -2740,6 +2780,9 @@ impl Cx {
self.os.timers.timers.remove(&timer_id);
}
CxOsOp::ShowTextIME(_area, _pos, config) => unsafe {
// An input asking for the keyboard right after a hide (e.g., it just took
// focus from another input) keeps the keyboard up.
self.os.pending_ime_hide = false;
// A focused `TextInput` re-issues `ShowTextIME` on every
// draw. Calling into Java each time thrashes the IME:
// `configure_keyboard` can restart the input connection,
Expand All @@ -2749,19 +2792,19 @@ impl Cx {
// — a loop that flickers the soft keyboard open then shut.
// Only touch Java when the requested config actually
// changes; `last_ime_config` is cleared whenever the
// keyboard goes down (here or via `ResizeTextIME`).
// keyboard goes down (once a pending hide goes through,
// or via `ResizeTextIME`).
if self.os.last_ime_config != Some(config) {
android_jni::to_java_configure_keyboard(&config);
android_jni::to_java_show_keyboard(true);
self.os.last_ime_config = Some(config);
}
},
CxOsOp::HideTextIME => unsafe {
CxOsOp::HideTextIME => {
// Unconditional on purpose: unlike `ShowTextIME` this is not
// issued per-frame, so there is no thrash to dedup — and a
// skipped hide would leave the soft keyboard stuck open.
android_jni::to_java_show_keyboard(false);
self.os.last_ime_config = None;
self.os.pending_ime_hide = true;
},
CxOsOp::SyncImeState {
text,
Expand Down Expand Up @@ -3715,6 +3758,7 @@ impl Default for CxOs {
last_ime_height: 0.0,
last_ime_visible: false,
last_ime_config: None,
pending_ime_hide: false,
media: CxAndroidMedia::default(),
display: None,
surface_alive: false,
Expand Down Expand Up @@ -3857,9 +3901,12 @@ pub struct CxOs {
/// or `None` while the keyboard is requested-hidden. A focused `TextInput`
/// re-issues `ShowTextIME` every draw; this dedups those so the JNI IME
/// calls (and the inset-driven redraw loop they trigger) fire only on a
/// real change. Reset to `None` on `HideTextIME` and when Java reports the
/// keyboard closed.
/// real change. Reset to `None` once a `HideTextIME` goes through and when
/// Java reports the keyboard closed.
pub last_ime_config: Option<TextInputConfig>,
/// Set by `HideTextIME`, which waits until after the next draw, so that moving focus
/// from one text input to another keeps the keyboard up instead of hiding it.
pub pending_ime_hide: bool,
pub frame_time: i64,
pub quit: bool,
pub fullscreen: bool,
Expand Down
Loading