* Introduce knowledge of device screen bounds/cutous/"safe inset areas"
Tested working on iOS, implemented for Android but not yet tested.
The approach may need to be improved, because it currently restricts
the whole app window to being fully within the safe areas.
We may not necessarily want that, or if we do, then we probably also
need to support setting the base color of the reserved system areas
(beneath the app bounds and above in the notification bar area).
* Use metal scissor rect to prevent SVGs/icons from being mis-drawn in safe areas
This prevents anything from being accidentally drawn in the safe
inset areas when the pass clear_color is transparent. Of course,
we can still draw the pass clear_color in those areas.
* Workaround: apply a scissor rect within safe inset area
Only apply it to clip any DrawSvg/DrawVector-specific draw calls
within the safe inset area.
This is unfortunately still just a hacky solution, because if we
actually do want to draw svg/vectors within that safe inset area,
then we won't be able to.
* Properly fix gpu artifacts when rendering SVGs
The `DrawSvg` vertex shader had a GPU fringe expansion pass designed for `fill_gpu()` mode, where fringe vertices encode per-vertex normals in the `v` and `stroke_dist` fields. SVG rendering used `fill_gpu()`, which produces **coincident-vertex fringe triangles** (body and outer fringe at the same CPU position, expanded on the GPU). These zero-area triangles caused **Metal GPU rasterization artifacts** — stray fragments appearing at unexpected screen positions.
**`draw/src/svg/render.rs`** — Switch SVG fill from `fill_gpu()` to `fill()`. Pre-computed fringe produces vertices at physically different positions (no coincident vertices).
**`draw/src/shader/draw_svg.rs`** — Remove the GPU fringe expansion code from the vertex shader. With pre-computed fringe, the `v` and `stroke_dist` fields are constants (`1.0` and `0.0`), not per-vertex normals. The expansion code was misinterpreting `v=1.0` as a horizontal normal, corrupting vertex positions.
**`libs/apple_sys/src/lib.rs`** — Added `MTLScissorRect` struct (unused now but available for future use).
**`src/home/rooms_sidebar.rs`** — Changed shadow offset from `vec2(1.0, 0.0)` to `vec2(0.0, 10.0)` so the `RoundedShadowView` shadow only draws below the header, eliminating the gray line at the top of the screen (issue 1).
* Expose safe area inset padding to app, don't forcibly apply it to root window
* Fixed safe area insets padding, with support for rotation
We now make these values available to the app dev (see below)
instead of forcibly inserting them as padding on all root windows.
This will allow each app to choose how and when they want to apply said pad values
(or if they want to at all) in an easy way, both at the Splash level
or more dynamically/programmatically at the Rust level.
Required quite a few changes to how things work in the iOS platform plumbing,
also described below in the generated summary:
On iOS and Android, Makepad apps render content behind device cutouts (Dynamic Island, camera notch), home indicators, and rounded screen corners because the framework has no awareness of safe area insets.
Added platform-level safe area inset querying on iOS and Android, exposed the values through both the Splash DSL (`mod.widgets.SAFE_INSET_PAD_*`) and Rust (`cx.display_context.safe_area_insets`), and ensured they update correctly on device rotation.
**New types:**
- `UIEdgeInsets` struct in `libs/apple_sys` for Objective-C interop
- `SafeAreaInsets` struct in `platform/src/event/window.rs` (top/right/bottom/left in logical points)
- Added `safe_area_insets` field to `WindowGeom` and `DisplayContext`
**iOS (`platform/src/os/apple/ios/`):**
- Query `[UIView safeAreaInsets]` from the MTKView in `check_window_geom()`
- Added `viewSafeAreaInsetsDidChange` callback on `MakepadViewController` to detect inset changes on rotation
- Populate `display_context` before `Event::Startup` so values are available during app script initialization
- Fixed MTKView setup: removed redundant `addSubview:` (conflicted with `setRootViewController:`) and added autoresizing mask — both required for safe area propagation on rotation
**Android (`platform/src/os/linux/android/`):**
- Added `SafeAreaInsets` variant to `FromJavaMessage` and corresponding JNI function
- Java side (`ResizingLayout.onApplyWindowInsets`): queries `WindowInsets.Type.systemBars() | displayCutout()` and sends insets to Rust (converted from px to dp)
- Added `safe_area_insets` field to `CxOs`, populated on `SafeAreaInsets` message and included in `WindowGeom` construction
- Added `surfaceOnSafeAreaInsets` native method to `MakepadNative.java`
**Splash DSL variables (`widgets/src/lib.rs`):**
- `mod.widgets.SAFE_INSET_PAD_TOP`
- `mod.widgets.SAFE_INSET_PAD_BOTTOM`
- `mod.widgets.SAFE_INSET_PAD_LEFT`
- `mod.widgets.SAFE_INSET_PAD_RIGHT`
- Values read from `display_context` at widget module initialization (during `Event::Startup`)
- Updated on the script heap via `Cx::update_safe_inset_script_values()` on `WindowGeomChange`
**Rotation support:**
- Added `pending_script_reapply` flag on `Cx` — set when safe area insets change, checked at the end of the platform event loop iteration
- Fires a deferred `LiveEdit` event to re-evaluate and re-apply all Splash widget definitions with updated inset values
- Implemented in both iOS and Android event loops
**StackNavigationView fix (`widgets/src/stack_navigation.rs`):**
- Full-screen stack views now position at `max(safe_area_insets.top, parent_rect.pos.y)` instead of hardcoded `y: 0`, respecting both mobile safe areas and desktop title bars
**All other platforms:**
- Added `..Default::default()` to all `WindowGeom` constructors (macOS, Windows, Linux X11/Wayland/Direct, web, tvOS, OpenHarmony) so the new `safe_area_insets` field defaults to zeros
* Additional opptimizations for windows shader compilation
`hlsl_compile_shaders` was called unconditionally after every draw event, even on
frames where no new shaders needed compilation. Added an early return:
```rust
if self.draw_shaders.compile_set.is_empty() {
return;
}
```
The loop previously collected `compile_set` into a temporary `Vec` before
iterating, in order to release the borrow on `compile_set` so the loop body
could mutate other `draw_shaders` fields. This caused a heap allocation and a
full copy of all indices on every compilation batch.
`std::mem::take` atomically replaces `compile_set` with an empty `BTreeSet` and
returns ownership of the original — no intermediate allocation, no copy, and no
separate `.clear()` needed at the end:
```rust
let compile_set = std::mem::take(&mut self.draw_shaders.compile_set);
for draw_shader_id in compile_set { ... }
// no .clear() needed
```
Previously `shader_cache_dir()` was an inner function called inside
`CxOsDrawShader::new`, meaning it ran once **per shader** on every compilation.
Each call performs two syscalls: `env::var("LOCALAPPDATA")` and
`fs::create_dir_all`. With N shaders compiling on first launch, this was 2N
unnecessary syscalls.
`shader_cache_dir()` is now a module-level function called **once** before the
loop in `hlsl_compile_shaders`, and the resulting `Option<&Path>` is passed into
`new` as a parameter.
The HLSL source already lives in `cx_shader.mapping.code`. The previous code
cloned it into an owned `String` before passing it to `new`, even though all
downstream uses (hashing, `D3DCompile`, cache I/O, error printing) only need a
`&str`. Changed the parameter type to `&str` and restructured the loop body into
a block scope so the immutable borrow on `cx_shader` ends before the mutable
reborrow — eliminating the clone entirely.
`CxOsDrawShader::new` already took `&UniformBufferBindings` by reference. The
clone existed only because the immutable borrow on `cx_shader` had to be released
before the mutable reborrow. The same block-scope restructuring from fix#4
resolves this: `&cx_shader.mapping.uniform_buffer_bindings` is now passed
directly.
This field was written once on construction and **never read** — the only reader
was the O(n) deduplication scan removed in the previous round of fixes. It held
a full copy of each shader's HLSL source for the entire lifetime of the
application. At tens of KB per shader and dozens of shaders, this was megabytes
of permanently retained dead storage. The field is gone.
* More shader optimizations on windows
Properly get the Local AppData directory instead of using the
env var %LOCALAPPDATA, which may not always be there.
Now we do it with `SHGetKnownFolderPath(FOLDERID_LocalAppData)`,
which is canonically correct.
We also cache the directory path itself.
On Windows, a large app like Robrix freezes for 10–20+ seconds after login while sync begins.
Profiling (`sc.user_aux.etl` from Visual Studio Performance Profiler) showed:
| Module | Exclusive CPU samples | % of total |
|---|---|---|
| `d3dcompiler_47.dll` | 24,239 | **76.76%** |
| `robrix.exe` | 2,715 | 8.60% |
A single thread (TID 20308) consumed **27.9 seconds of CPU** over the 35-second trace.
Every other robrix thread combined used under 2 seconds.
The butterfly call graph confirmed: `robrix.exe → d3dcompiler_47.dll` with 25,337
inclusive hits (80.24%). The UI was blocked the entire time.
In `makepad/platform/src/os/windows/windows.rs`, the main Win32 event loop calls:
```rust
if self.need_redrawing() {
self.call_draw_event(time_now);
self.hlsl_compile_shaders(&d3d11_cx); // blocks here
}
```
`hlsl_compile_shaders` iterates over every shader in `compile_set` and calls
`CxOsDrawShader::new`, which calls `D3DCompile` (from `d3dcompiler_47.dll`)
**synchronously on the UI thread** for each unique shader. After login, many
new UI panels render for the first time, flooding `compile_set`. `D3DCompile`
is a full software HLSL→DXBC compiler with no OS-level cache — it is CPU-bound
and cannot yield.
This affects all makepad apps on Windows, not just Robrix.
**File changed:** `makepad/platform/src/os/windows/d3d11.rs`
Added a disk-based shader bytecode cache so that `D3DCompile` is only called
once per unique shader source, on first launch. Subsequent launches load the
pre-compiled DXBC bytecode directly, skipping `D3DCompile` entirely.
**Specific changes:**
1. `CxOsDrawShader` struct: changed `pixel_shader_blob` and `vertex_shader_blob`
field types from `ID3DBlob` to `Vec<u8>`. These fields were stored but never
read after construction, so there is no behavioral difference.
2. `compile_shader` (inner fn): changed return type from `ID3DBlob` to `Vec<u8>`,
copying the blob bytes out before returning.
3. Three new inner helper functions added to `CxOsDrawShader::new`:
- `hlsl_cache_key(hlsl: &str) -> u64` — FNV-1a 64-bit hash of the HLSL
source string, stable across Rust versions, used as the cache key.
- `shader_cache_dir() -> Option<PathBuf>` — resolves
`%LOCALAPPDATA%\makepad\d3d11_shader_cache\`, creating it if needed.
Returns `None` gracefully if `LOCALAPPDATA` is unset or the directory
cannot be created, in which case compilation proceeds as before.
- `get_shader_bytes(...)` — checks for a cached `<hash>_vs.dxbc` /
`<hash>_ps.dxbc` file; on a cache miss, compiles via `D3DCompile` and
writes the result to disk before returning.
- **First launch:** all shaders compile as before; each VS/PS blob is written to
`%LOCALAPPDATA%\makepad\d3d11_shader_cache\<hash>_vs.dxbc` and `<hash>_ps.dxbc`.
- **Subsequent launches:** bytecode is read from disk; `CreateVertexShader` /
`CreatePixelShader` / `CreateInputLayout` are called directly with the cached
bytes — `D3DCompile` is never invoked.
- **Cache invalidation:** the cache key is the FNV-1a hash of the HLSL source,
so entries automatically become stale (and are recompiled + re-cached) whenever
the shader source changes.
- **Failure safety:** file I/O errors are silently ignored — a failed write means
the cache is just skipped next time, and a failed read falls through to
recompilation.