makepad/libs/live_id
Kevin Boos 9e5e3d2b03
Faster shader codegen, working shader caches, and no GL repaints for buffer retirement (#1279)
* Script: skip "did you mean" text for lookups that are expected to miss

Shader codegen looks up `self.<field>` on the draw object before falling back to
the Rust instance fields, so every miss built a `suggest_property` message
(levenshtein across the whole proto chain) that the caller then threw away.
That was about half the cost of instantiating widgets that need new draw shaders.

* Those lookups, plus `proto_field_from_value` and a few VM reads that also
  clear the error and fall back, now pass `NoTrap`
* Real misses still report the same errors

* Text: cache rustybuzz shape plans per font face

`rustybuzz::shape` compiles a fresh `ShapePlan` on every shaping-cache miss,
which re-reads the font's GSUB/GPOS feature tables each time, so the first draw
of any new text paid for that over and over.

* `FontFace::shape` keeps one plan per (direction, script, language, features),
  built from the same inputs `rustybuzz::shape` uses, so the output is identical
* `set_variations` drops the plans along with the cached face

* Script: cut `format_object_for_error`'s preview at a char boundary

`format_object_for_error` cut its debug text with `truncate(197)`, a byte offset, so
building the message panicked when that byte landed inside a multibyte char.

It now counts chars instead, which leaves ASCII output unchanged.

* PageFlip: add `PageFlipRef::page()`

Callers could already build a lazy page through `PageFlipRef::set_active_page()`, but
that also shows it. This wrapper lets them build one ahead of time without showing it.

* GL: read the program cache before starting a parallel compile

`begin_state` returned `Pending` on parallel-compile drivers before reading the program-binary
cache, so those programs recompiled and rewrote their cache file on every launch.

* Read the cache first on both paths, so a hit is ready right away and isn't rewritten
* Delete a cached binary that won't load, since a driver that can't write one won't replace it
* Check `GL_EXTENSIONS` once per GL context instead of once per program
* Fold `GlShader::new` into `begin_state` and drop the Android "version conflict" check,
  which compared two identically built filenames

* Vulkan: build the XR variant only on Android, after the code-dedup check

Every new draw shader ran the full WGSL front-end plus naga twice, once for the window
variant and once for the XR variant, and did it before the code-dedup check, so a
duplicate paid for both and then threw the SPIR-V away.

* Only Android ever draws the XR variant, so other targets build just the window one
* Build the SPIR-V after the dedup check, since a dedup hit reuses the existing
  shader's `vulkan_shader` through its `os_shader_id`

* Vulkan: keep one `VkPipelineCache` per device

Every pipeline was created with `vk::PipelineCache::null()`, even though each swapchain
recreation (every Wayland resize, for one) destroys and rebuilds all of them.

* Create the cache in all three constructors, right before `CxVulkan` is built so the
  earlier error paths don't need to free it, and destroy it in `Drop` before the device
* Pass it to both `create_graphics_pipelines` calls
* It only lives in memory, and a failed create falls back to the null cache

* D3D11: drop a cached DXBC the device rejects

A cached DXBC that `CreateVertexShader`/`CreatePixelShader` rejected became a device
error, so the shader was requeued and re-read the same file forever.

* When the device is alive and not out of memory, delete that entry and compile once
  without the cache; a second rejection is a compile error that isn't requeued
* Drop the never-read shader blobs, and log codegen errors like the other backends

* Android: stop spawning uname -r at startup

`MakepadActivity.onCreate` spawned `uname -r` only to put the kernel version into the GL
program-binary cache filename.

* Pass an empty string instead, so the `onAndroidParams` signature doesn't change
* Key the cache files on the Android version, build and a hash of `GL_VENDOR`,
  `GL_RENDERER` and `GL_VERSION`, and drop `AndroidParams::kernel_version`
* The first cache write in a context deletes any other `shader_*.bin`, like the old
  `_kv` files or the ones an OS or driver update left behind

* LiveId: hash the interner's map by the id itself

A `LiveId` is already a hash, but the interner's map ran SipHash over it on
every lookup, and displaying a `LiveId` always does a lookup.

* Use `LiveIdMap`, which hashes by the id itself like the other `LiveId`-keyed maps
* Nothing iterates this map, so its order doesn't matter

* LiveId: write interned names straight to the formatter

`Display` wrote an interned name with `write!(f, "{}", id)`, which runs a whole
nested `fmt::write` pass just to copy the string. `f.write_str(id)` writes the same
bytes, since that nested pass never applied the outer width or fill anyway.

* Script: write shader identifiers straight into the output

Local, param, io and field names each went through a throwaway `String`
(`map_local_name` even built two for GLSL), which the caller then formatted again.

* Add `write_local_name`, `write_param_name`, `write_io_name` and
  `write_field_name_typed`, which write into the caller's buffer, and use them
  on the front-end's hot paths and in `glsl_push_field`
* Replace the ten copies of the `ShaderIoPrefix` match with `write_prefixed_io_name`
* The `map_*` versions now just wrap them, for callers that keep the name around

* Script: register the backend's builtin ids once per shader

`compile_fn` ran `register_ids` for every function it compiled, including each
inlined call, and each `id_lut!` there hashes the name and takes the interner lock
only to find it's already registered. Every backend runs `pre_collect_shader_io`
once per shader before compiling any function, so register them there instead.
That cuts codegen time by ~1.5% for GLSL and ~8% for Metal and HLSL.

* Draw shaders: hash generated code with a faster hasher in the dedup cache

`cache_code_to_shader` ran SipHash over each new shader's whole generated source
to look it up, again to insert it, and over every entry whenever the map grew.

* Use foldhash's `RandomState`, which hashes these sources ~7x faster
* foldhash is already vendored, and built on Linux and Android through naga

* GL: hash program cache keys faster

`program_cache_path` hashed both full shader sources one byte at a time with
`LiveId::str_append`, which took ~3 ms of each Android launch with a warm cache.

* Use foldhash's `FixedState` (~150x faster on our generated GLSL), whose fixed
  seed gives the same build the same file on every launch
* Add a marker from that hasher to the Android suffix, so the stale-cache sweep
  also deletes binaries saved under an older key, like the `LiveId` ones
* Only hash sources for the `gl.shader_builds` trace logs when that tracing is on

* GL: flush the lifetime fence when an explicit poll checks it

An explicit poll (`frame_completion_serial`, texture release) can run at rest with no swap
coming to flush the fence it made, and per the ES spec `glFlush` alone isn't guaranteed to
get it to the GPU. Explicit polls now check the fence with `SYNC_FLUSH_COMMANDS_BIT`, which
is. The paint path's poll is unchanged.

* GL: drain buffer retirement on quiet beats instead of repainting

While retirement work was pending, `render_view` set `demo_time_repaint`, so every window
re-rendered and swapped each frame just to step the bookkeeping. On Android that was about
40 full renders in a short Robrix session, half of its paint time in the first 5 s.
* Like Metal, `maintain_instance_retirements` now runs that step on beats that render nothing,
  then flushes its GL deletes: Android's closed draw gate, X11/Wayland's idle return, skipped
  passes and the Studio-hosted child.
* If the lifetime poll can't see our context it asks for a repaint instead, as do builds
  without the fence (DRM, OHOS, Android's GL fallback under `use_vulkan`).

* TextInput: start in the right empty state instead of fading into it

A new empty input played `empty.on`, a 0.2 s fade out of the not-empty look, so building
a page of inputs kept the UI animating even while that page was hidden. `on_after_new`
now cuts straight to the state, and edits still fade.

* PortalList: don't scroll or redraw for a drag move that didn't move

Android keeps sending moves for a still finger, so pressing on a list's background redrew
the whole window every frame until the finger lifted.

* Script: find the functions a GLSL stage needs from recorded calls

`glsl_write_functions_for_entries` found which functions a stage calls by searching
every reachable body for every other function's name, `format!`ing a new pattern for
each probe, and it ran for both stages of every shader.

* `compile_shader_def` now also returns the callee's index, and each `ShaderFn` keeps
  the `callees` its body calls
* The GLSL backend walks those from the entry instead, which emits the same functions
  in the same order and cuts GLSL codegen time by ~16%

* GL: note the GPU adapter even when the program cache is warm

`begin_state` returns a cached program before it reaches `start_pending_program_compile`,
so with a warm cache `system_info::gpu_adapter_name()` stayed `None` (the feedback panel
showed "unknown adapter") and the "Makepad GL" driver line was never logged. That once-only
block now runs at the top of `begin_state`, so the first program does it either way.

* Shorten a few comments

The GL paint path's fence comment and `poll_texture_lifetimes_for`'s doc each ran to four
lines, and `PortalList` and `MakepadActivity` each had a comment line over 100 columns.
2026-10-02 01:21:31 +02:00
..
id_macros compile 2026-02-10 18:26:51 +01:00
src Faster shader codegen, working shader caches, and no GL repaints for buffer retirement (#1279) 2026-10-02 01:21:31 +02:00
Cargo.toml Add optional serde derives for Serialization/Deserialization on select public types (#831) 2025-12-18 08:38:58 +01:00