diff --git a/iris/core/assets/fonts/NotoSans-Bold.ttf b/iris/core/assets/fonts/NotoSans-Bold.ttf new file mode 100644 index 0000000..aae7546 Binary files /dev/null and b/iris/core/assets/fonts/NotoSans-Bold.ttf differ diff --git a/iris/core/assets/fonts/NotoSans-BoldItalic.ttf b/iris/core/assets/fonts/NotoSans-BoldItalic.ttf new file mode 100644 index 0000000..6f685b2 Binary files /dev/null and b/iris/core/assets/fonts/NotoSans-BoldItalic.ttf differ diff --git a/iris/core/assets/fonts/NotoSans-Italic.ttf b/iris/core/assets/fonts/NotoSans-Italic.ttf new file mode 100644 index 0000000..7f53133 Binary files /dev/null and b/iris/core/assets/fonts/NotoSans-Italic.ttf differ diff --git a/iris/core/assets/fonts/NotoSans-Regular.ttf b/iris/core/assets/fonts/NotoSans-Regular.ttf new file mode 100644 index 0000000..f27f4ff Binary files /dev/null and b/iris/core/assets/fonts/NotoSans-Regular.ttf differ diff --git a/iris/core/assets/fonts/NotoSansMono-Bold.ttf b/iris/core/assets/fonts/NotoSansMono-Bold.ttf new file mode 100644 index 0000000..9218a79 Binary files /dev/null and b/iris/core/assets/fonts/NotoSansMono-Bold.ttf differ diff --git a/iris/core/assets/fonts/NotoSansMono-Regular.ttf b/iris/core/assets/fonts/NotoSansMono-Regular.ttf new file mode 100644 index 0000000..159ca4b Binary files /dev/null and b/iris/core/assets/fonts/NotoSansMono-Regular.ttf differ diff --git a/iris/core/assets/fonts/OFL.txt b/iris/core/assets/fonts/OFL.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/iris/core/assets/fonts/OFL.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/iris/core/src/primitive/text.rs b/iris/core/src/primitive/text.rs index ae65f02..db50a02 100644 --- a/iris/core/src/primitive/text.rs +++ b/iris/core/src/primitive/text.rs @@ -2,14 +2,65 @@ use crate::{Align, GlyphAtlas, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiC use parley::{ Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, FontStyle, FontWeight, GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, + fontique::{Blob, FamilyId}, }; use std::ops::Range; +use std::sync::Arc; use swash::{ FontRef, scale::{Render, ScaleContext, Source, StrikeWith}, zeno::{Format, Vector}, }; +/// Bundled fonts, registered over the system collection rather than relied +/// on alone -- see `TextData::register_bundled_fonts`'s doc comment for +/// why. Static weight/style cuts, not a variable font: parley/fontique +/// resolve a variable font's weight axis by picking normalized coordinates +/// on whatever single face registers for the family, and a phone whose +/// system "Roboto" is actually the variable "Roboto Flex" is exactly the +/// device class this sidesteps, rather than depends on working correctly. +/// Noto Sans, OFL-licensed (`assets/fonts/OFL.txt`), chosen for coverage +/// breadth (a transcript's content is not known in advance) over a +/// smaller-footprint alternative -- see the doc comment for the size this +/// added. +const NOTO_SANS_REGULAR: &[u8] = include_bytes!("../../assets/fonts/NotoSans-Regular.ttf"); +const NOTO_SANS_BOLD: &[u8] = include_bytes!("../../assets/fonts/NotoSans-Bold.ttf"); +const NOTO_SANS_ITALIC: &[u8] = include_bytes!("../../assets/fonts/NotoSans-Italic.ttf"); +const NOTO_SANS_BOLD_ITALIC: &[u8] = + include_bytes!("../../assets/fonts/NotoSans-BoldItalic.ttf"); +const NOTO_SANS_MONO_REGULAR: &[u8] = + include_bytes!("../../assets/fonts/NotoSansMono-Regular.ttf"); +const NOTO_SANS_MONO_BOLD: &[u8] = include_bytes!("../../assets/fonts/NotoSansMono-Bold.ttf"); + +/// What starting up found about text rendering, for the on-screen +/// Diagnostics page and the one startup log line (RUST.md's P0 box, "log +/// once at startup ... the number of font families found, the default +/// family resolved"). Built once by `TextData::font_diagnostics` -- +/// `Default::default` still exists for callers (tests, examples) that +/// don't need the report. +#[derive(Clone, Debug)] +pub struct FontDiagnostics { + /// `Collection::family_names().count()` after registering the bundled + /// fonts -- system families plus the two bundled ones. + pub families_found: usize, + /// The family `GenericFamily::SansSerif` resolves to first -- the + /// bundled "Noto Sans" unless registration itself failed. + pub default_family: Option, + /// The family `GenericFamily::Monospace` resolves to first. + pub default_mono_family: Option, + /// One resolved family name per style axis this crate actually uses + /// (`SpanStyle::bold`/`italic`), so a report can say plainly whether a + /// bold/italic request is landing on a real face rather than being + /// silently absorbed by whatever the sans-serif default resolves to + /// for every weight (RUST.md's P0 box, "bold words render as blank + /// gaps" -- a family that resolves but has no distinct bold face is + /// exactly what produced that). + pub regular_resolved: Option, + pub bold_resolved: Option, + pub italic_resolved: Option, + pub mono_resolved: Option, +} + /// Everything text needs that outlives one string: the font collection, the /// layout scratch space, the glyph rasteriser and the atlas they fill. pub struct TextData { @@ -21,11 +72,154 @@ pub struct TextData { impl Default for TextData { fn default() -> Self { - Self { + let mut data = Self { font_cx: FontContext::new(), layout_cx: LayoutContext::new(), scale_cx: ScaleContext::new(), atlas: GlyphAtlas::default(), + }; + data.register_bundled_fonts(); + data + } +} + +impl TextData { + /// Registers Noto Sans (regular/bold/italic/bold-italic) and Noto Sans + /// Mono (regular/bold) as static faces, and puts them **first** in the + /// `SansSerif`/`Monospace` generic-family fallback lists -- ahead of, + /// not instead of, whatever the platform already found, so a script + /// Noto Sans lacks (CJK, emoji, ...) still falls through to the system + /// font the same as before this existed. + /// + /// Exists because text rendering must not depend on the platform's own + /// font enumeration succeeding or resolving weight/style the way this + /// crate assumes: RUST.md's P0 box found bold spans on a real phone + /// rendering as blank gaps of the correct advance width (the glyph + /// simply wasn't rasterised -- `TextData::place`'s `None` arm), while + /// the emulator's system fonts happened to resolve every style. A + /// bundled, static-per-style family removes fontique's Android font + /// scan (`fontique::backend::android::SystemFonts::new`, which parses + /// `/system/fonts` and `/system/etc/fonts.xml`) from the path a glyph + /// has to survive to reach the screen at all. + /// + /// Cost: six static `.ttf`s, ~3.6 MB uncompressed + /// (`iris/core/assets/fonts/`), landing in the APK compressed -- + /// `build-apk.sh`'s own output is what says the delivered number, not + /// this comment. + fn register_bundled_fonts(&mut self) { + fn register(cx: &mut FontContext, bytes: &'static [u8]) -> Option { + let blob = Blob::new(Arc::new(bytes)); + cx.collection + .register_fonts(blob, None) + .into_iter() + .map(|(id, _)| id) + .next() + } + let sans_id = register(&mut self.font_cx, NOTO_SANS_REGULAR); + register(&mut self.font_cx, NOTO_SANS_BOLD); + register(&mut self.font_cx, NOTO_SANS_ITALIC); + register(&mut self.font_cx, NOTO_SANS_BOLD_ITALIC); + let mono_id = register(&mut self.font_cx, NOTO_SANS_MONO_REGULAR); + register(&mut self.font_cx, NOTO_SANS_MONO_BOLD); + + if let Some(sans_id) = sans_id { + let existing: Vec<_> = self + .font_cx + .collection + .generic_families(GenericFamily::SansSerif) + .collect(); + self.font_cx.collection.set_generic_families( + GenericFamily::SansSerif, + std::iter::once(sans_id).chain(existing), + ); + let existing: Vec<_> = self + .font_cx + .collection + .generic_families(GenericFamily::SystemUi) + .collect(); + self.font_cx.collection.set_generic_families( + GenericFamily::SystemUi, + std::iter::once(sans_id).chain(existing), + ); + } + if let Some(mono_id) = mono_id { + let existing: Vec<_> = self + .font_cx + .collection + .generic_families(GenericFamily::Monospace) + .collect(); + self.font_cx.collection.set_generic_families( + GenericFamily::Monospace, + std::iter::once(mono_id).chain(existing), + ); + } + } + + /// Builds the startup report -- see `FontDiagnostics`. Queries the + /// collection directly (`fontique::Query`) rather than shaping a real + /// string, since all that's needed is which family each axis lands on. + pub fn font_diagnostics(&mut self) -> FontDiagnostics { + use parley::fontique::{Attributes, FontWidth, QueryStatus}; + let families_found = self.font_cx.collection.family_names().count(); + let default_family_id = self + .font_cx + .collection + .generic_families(GenericFamily::SansSerif) + .next(); + let default_family = default_family_id + .and_then(|id| self.font_cx.collection.family_name(id).map(str::to_string)); + let default_mono_family_id = self + .font_cx + .collection + .generic_families(GenericFamily::Monospace) + .next(); + let default_mono_family = default_mono_family_id + .and_then(|id| self.font_cx.collection.family_name(id).map(str::to_string)); + + // Resolves the family a (generic family, weight, style) query lands + // on, without holding the `Query`'s borrow of `collection` across + // the `family_name` lookup that needs it back -- the `FamilyId` is + // captured out of the closure first, then looked up once `query` + // (and its borrow) has been dropped. + let mut resolve_family = + |generic: GenericFamily, weight: FontWeight, style: FontStyle| -> Option { + let mut family_id = None; + { + let mut query = self + .font_cx + .collection + .query(&mut self.font_cx.source_cache); + query.set_families([generic]); + query.set_attributes(Attributes { + width: FontWidth::NORMAL, + style, + weight, + }); + query.matches_with(|font| { + family_id = Some(font.family.0); + QueryStatus::Stop + }); + } + family_id.and_then(|id| self.font_cx.collection.family_name(id).map(str::to_string)) + }; + + let regular_resolved = + resolve_family(GenericFamily::SansSerif, FontWeight::NORMAL, FontStyle::Normal); + let bold_resolved = + resolve_family(GenericFamily::SansSerif, FontWeight::BOLD, FontStyle::Normal); + let italic_resolved = + resolve_family(GenericFamily::SansSerif, FontWeight::NORMAL, FontStyle::Italic); + let mono_resolved = + resolve_family(GenericFamily::Monospace, FontWeight::NORMAL, FontStyle::Normal); + + FontDiagnostics { + families_found, + default_family, + default_mono_family, + regular_resolved, + bold_resolved, + italic_resolved, + mono_resolved, } } } diff --git a/iris/core/src/render/mod.rs b/iris/core/src/render/mod.rs index 1eb4e79..1b8dd12 100644 --- a/iris/core/src/render/mod.rs +++ b/iris/core/src/render/mod.rs @@ -66,6 +66,57 @@ pub fn device_limits() -> Limits { } } +/// A capped log of wgpu's *uncaptured* errors -- everything that reaches +/// `Device::on_uncaptured_error` rather than one of `UiRenderNode::new`'s +/// own error scopes, i.e. every wgpu error raised outside device/pipeline +/// creation: a validation failure during an ordinary frame's `update`/ +/// `draw`, for instance. wgpu's default handler for these is `panic!` with +/// no caller able to intervene -- exactly what aborted the P0 bench APK +/// once already (this file's `UiRenderNode::new` doc comment) -- so both +/// platform backends install a handler here instead of leaving the default +/// in place, per RUST.md's P0 box ("every wgpu uncaptured error ... it +/// must never panic in release"). +/// +/// Cheap to `Clone` (an `Arc` around the real storage) rather than a +/// process-wide static, so a caller builds one alongside its `Device`, +/// hands one clone to `on_uncaptured_error`'s closure and keeps the other +/// for the Diagnostics page to read -- context passed explicitly, per +/// AGENTS.md/CODE_RULES.md's "no globals" rather than reached for through a +/// `OnceLock`. +#[derive(Clone)] +pub struct WgpuErrorLog { + errors: std::sync::Arc>>, +} + +/// How many uncaptured errors the log keeps -- old ones drop off the front +/// rather than being trimmed on read, so a build spraying errors every +/// frame doesn't grow this without bound. +const WGPU_ERROR_LOG_CAP: usize = 20; + +impl Default for WgpuErrorLog { + fn default() -> Self { + Self { + errors: std::sync::Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new())), + } + } +} + +impl WgpuErrorLog { + pub fn record(&self, error: impl std::fmt::Display) { + let mut errors = self.errors.lock().unwrap(); + if errors.len() >= WGPU_ERROR_LOG_CAP { + errors.pop_front(); + } + errors.push_back(error.to_string()); + } + + /// A snapshot for the Diagnostics page -- cloned rather than held, + /// since the lock must not outlive one call. + pub fn snapshot(&self) -> Vec { + self.errors.lock().unwrap().iter().cloned().collect() + } +} + pub struct UiRenderNode { uniform_group: BindGroup, primitive_layout: BindGroupLayout, @@ -153,7 +204,7 @@ impl UiRenderNode { queue: &Queue, ui: &mut UiData, ui_render: &mut UiRenderState, - ) { + ) -> FrameUpdateStats { self.active.clear(); for (i, primitives) in ui_render.layers.iter_mut() { self.active.push(i); @@ -237,6 +288,10 @@ impl UiRenderNode { if rebuild_main { self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures); } + FrameUpdateStats { + masks_resized, + moves_resized, + } } /// Takes a size rather than a window type: this is the only thing the @@ -273,6 +328,7 @@ impl UiRenderNode { device: &Device, queue: &Queue, config: &SurfaceConfiguration, + window_size: impl Into, ) -> Result { // Popped in reverse of this order, once every creation call below // has run -- `Device::push_error_scope`'s own contract. @@ -285,20 +341,35 @@ impl UiRenderNode { source: ShaderSource::Wgsl(SHAPE_SHADER.into()), }); - // Seeded from the surface's own size, not `WindowUniform::default()` - // (0, 0): the vertex shader divides by `window.dim` to reach clip - // space, so a window this buffer disagrees with means every - // primitive's position is NaN/Inf and is dropped before - // rasterization -- the clear colour still reaches the screen (the - // pass runs regardless) while nothing drawn on top of it ever does. - // winit's backend gets away with the old default because winit - // fires an initial `WindowEvent::Resized` that calls `resize()` - // before the first frame; android-view has no such automatic - // event, so `AndroidRenderer::new` built a node whose window buffer - // was never corrected -- this is I2's "nothing draws" bug (RUST.md). - let window_uniform = WindowUniform { - width: config.width as f32, - height: config.height as f32, + // Seeded from the caller's own reported size, not + // `WindowUniform::default()` (0, 0): the vertex shader divides by + // `window.dim` to reach clip space, so a window this buffer + // disagrees with means every primitive's position is NaN/Inf and is + // dropped before rasterization -- the clear colour still reaches + // the screen (the pass runs regardless) while nothing drawn on top + // of it ever does. winit's backend gets away with the old default + // because winit fires an initial `WindowEvent::Resized` that calls + // `resize()` before the first frame; android-view has no such + // automatic event, so `AndroidRenderer::new` built a node whose + // window buffer was never corrected -- this is I2's "nothing draws" + // bug (RUST.md). + // + // **Deliberately not `config.width`/`config.height`**: those are + // the surface's *physical* pixel size, which the swapchain needs, + // but everything downstream of this uniform (layout, hit-testing, + // glyph/rect positions) works in the caller's own units -- on + // Android that's *logical* (physical / density) since RUST.md's P0 + // box ("text is far too small"), on desktop it's whatever + // `default::render::UiRenderer::new` already divides by + // `window.scale_factor()`. Passing it in explicitly, rather than + // deriving it from `config` here, is what keeps this crate from + // needing to know either platform's notion of density at all. + let window_uniform = { + let size = window_size.into(); + WindowUniform { + width: size.x, + height: size.y, + } }; let window_buffer = device.create_buffer_init(&BufferInitDescriptor { label: Some("window"), @@ -593,4 +664,26 @@ impl UiRenderNode { pub fn take_image_bind_group_creates(&mut self) -> u64 { self.textures.take_bind_group_creates() } + + /// Atlas-array `grow_array` calls since the last call -- same calling + /// convention as `take_image_bind_group_creates` (call once per frame, + /// before `update()`, to read exactly the previous frame's tally). Part + /// of the Diagnostics page's per-frame report (RUST.md's P0 box, "the + /// first input frame" investigation): if a report ever shows a grow + /// landing on the same frame the glyphs vanished, that is the + /// coincidence to chase first. + pub fn take_atlas_pages_grown(&mut self) -> u64 { + self.textures.take_pages_grown() + } +} + +/// What `UiRenderNode::update` changed this frame that a caller building a +/// per-frame diagnostic report cares about -- see `take_image_bind_group_creates`/ +/// `take_atlas_pages_grown` for the two counters this doesn't carry (they +/// use the existing "call before update()" convention instead, so as not +/// to disturb `bench_images`' documented counts). +#[derive(Clone, Copy, Debug, Default)] +pub struct FrameUpdateStats { + pub masks_resized: bool, + pub moves_resized: bool, } diff --git a/iris/core/src/render/texture.rs b/iris/core/src/render/texture.rs index ef67ba6..03fe4dd 100644 --- a/iris/core/src/render/texture.rs +++ b/iris/core/src/render/texture.rs @@ -67,6 +67,12 @@ pub struct GpuTextures { /// unchanging image list is zero, the same way `UiRenderState`'s /// `draw_count`/`region_mut_count` prove the layout side. bind_group_creates: u64, + /// `grow_array` calls since the last `take_pages_grown` -- the + /// Diagnostics page's per-frame report (RUST.md's P0 box, "the first + /// input frame" investigation) reads this alongside `bind_group_creates` + /// to say whether *this* frame's glyph disappearance, if any, coincided + /// with the atlas array being recreated. + pages_grown: u64, } impl GpuTextures { @@ -226,6 +232,7 @@ impl GpuTextures { /// array's view, which invalidates every bind group that referenced it, /// so this also rebuilds all of them before returning. fn grow_array(&mut self, rsc_layout: &BindGroupLayout) { + self.pages_grown += 1; let new_capacity = self.array_capacity * 2; let new_texture = Self::create_array_texture(&self.device, new_capacity); if self.page_count > 0 { @@ -392,6 +399,7 @@ impl GpuTextures { sampler, null_view, bind_group_creates: 0, + pages_grown: 0, } } @@ -402,6 +410,12 @@ impl GpuTextures { std::mem::take(&mut self.bind_group_creates) } + /// Reads and zeroes the atlas-array-grow counter -- see `pages_grown`'s + /// field comment. + pub fn take_pages_grown(&mut self) -> u64 { + std::mem::take(&mut self.pages_grown) + } + pub fn array_view(&self) -> &TextureView { &self.array_view } diff --git a/iris/src/default/input.rs b/iris/src/default/input.rs index 07b6bbf..7a0c475 100644 --- a/iris/src/default/input.rs +++ b/iris/src/default/input.rs @@ -11,10 +11,16 @@ pub struct Input { } impl Input { - pub fn event(&mut self, event: &WindowEvent) -> bool { + /// `scale_factor` converts winit's physical-pixel event coordinates + /// into the same logical units `UiRenderNode`'s window uniform now uses + /// (`default::render::UiRenderer::new`'s doc comment) -- without it, + /// a cursor position and the widget tree it's tested against would be + /// in two different units on any monitor whose scale factor isn't 1.0. + pub fn event(&mut self, event: &WindowEvent, scale_factor: f32) -> bool { match event { WindowEvent::CursorMoved { position, .. } => { - self.cursor.pos = Vec2::new(position.x as f32, position.y as f32); + self.cursor.pos = + Vec2::new(position.x as f32, position.y as f32) / scale_factor; self.cursor.exists = true; } WindowEvent::MouseInput { state, button, .. } => { @@ -30,7 +36,9 @@ impl Input { WindowEvent::MouseWheel { delta, .. } => { let mut delta = match *delta { MouseScrollDelta::LineDelta(x, y) => Vec2::new(x, y), - MouseScrollDelta::PixelDelta(pos) => Vec2::new(pos.x as f32, pos.y as f32), + MouseScrollDelta::PixelDelta(pos) => { + Vec2::new(pos.x as f32, pos.y as f32) / scale_factor + } }; if delta.x == 0.0 && self.modifiers.shift { delta.x = delta.y; @@ -68,8 +76,10 @@ impl Input { impl DefaultUiState { pub fn window_size(&self) -> Vec2 { - let size = self.renderer.window().inner_size(); - (size.width, size.height).into() + let window = self.renderer.window(); + let size = window.inner_size(); + let scale_factor = window.scale_factor() as f32; + Vec2::new(size.width as f32 / scale_factor, size.height as f32 / scale_factor) } pub fn cursor_state(&self) -> &CursorState { diff --git a/iris/src/default/mod.rs b/iris/src/default/mod.rs index 77f6411..d50a1b6 100644 --- a/iris/src/default/mod.rs +++ b/iris/src/default/mod.rs @@ -246,7 +246,8 @@ impl AppState for DefaultApp { ui_state .access_adapter .process_event(&ui_state.window, &event); - let input_changed = ui_state.input.event(&event); + let scale_factor = ui_state.renderer.window().scale_factor() as f32; + let input_changed = ui_state.input.event(&event, scale_factor); let cursor_state = ui_state.cursor_state().clone(); let old = ui_state.focus; if cursor_state.buttons.left.is_start() { diff --git a/iris/src/default/render.rs b/iris/src/default/render.rs index 44e9f48..4436852 100644 --- a/iris/src/default/render.rs +++ b/iris/src/default/render.rs @@ -1,5 +1,5 @@ use crate::task::RequestRedraw; -use iris_core::{UiData, UiRenderNode, UiRenderState}; +use iris_core::{UiData, UiRenderNode, UiRenderState, util::Vec2}; use pollster::FutureExt; use std::sync::Arc; use wgpu::*; @@ -66,7 +66,13 @@ impl UiRenderer { self.config.width = size.width; self.config.height = size.height; self.surface.configure(&self.device, &self.config); - self.ui.resize((size.width, size.height), &self.queue); + // Logical, matching `new`'s own seed -- see the comment there. + let scale_factor = self.window.scale_factor() as f32; + let logical = Vec2::new( + size.width as f32 / scale_factor, + size.height as f32 / scale_factor, + ); + self.ui.resize(logical, &self.queue); } fn create_encoder(device: &Device) -> CommandEncoder { @@ -147,7 +153,18 @@ impl UiRenderer { // by:" chain as the message, since `UiRenderNode::new` returns it // rather than letting wgpu's own default handler panic first (see // that function's doc comment). - let ui = UiRenderNode::new(&device, &queue, &config) + // Logical size (physical / `scale_factor`), matching what the + // Android backend now reports too (`android::render:: + // AndroidRenderer::new`, `content_scale`) -- the swapchain still + // configures at the real physical resolution above; only the + // window uniform layout/hit-testing agree on is scaled. Without + // this a window on any monitor whose scale factor isn't 1.0 would + // have the identical "everything too small" bug RUST.md's P0 box + // found on Iris's phone, just never noticed here because this + // crate's own dev monitors happen to run at 1.0. + let scale_factor = window.scale_factor() as f32; + let logical_size = Vec2::new(size.width as f32 / scale_factor, size.height as f32 / scale_factor); + let ui = UiRenderNode::new(&device, &queue, &config, logical_size) .expect("Could not create iris render node!"); Self {