Swap SipHash13 → AHash for the live-frame dedup key + record T10 plan

Two T10-flavoured changes in one commit, each independently ship-able
on its own:

1. `web_surface_frame::rgba_hash` switches from std's
   `DefaultHasher` (SipHash13, ~1.5 GB/s) to `ahash::AHasher`
   (~10 GB/s). At 1080p (8 MB per frame) the dedup key drops from
   ~5 ms to ~0.8 ms per cache-miss frame, returning roughly 25 % of
   the 16 ms scroll budget that was being spent hashing the
   newly-arrived RGBA payload. ahash was already in the dependency
   graph transitively via hashbrown, so this only adds a direct
   `ahash = "0.8"` line and one Cargo.lock entry.

2. `docs/t10-iosurface-plan.md` records the full architectural
   roadmap for the actual zero-copy path that supersedes the
   software-pipe pipeline: `OffscreenRenderingContext` against a
   hardware surfman adapter, IOSurface-backed surface on macOS,
   mach-port handoff to the GPUI process, MTLTexture import as an
   external sampler. The document explains why each currently
   shipped commit (`840255f`, `a80d039`, `e02c0fd`, `7f3b8b4`, plus
   this hash swap) is a stepping stone that eventually deletes
   itself once the IOSurface path lands, and names the upstream
   API gap in `servo-paint-api` that blocks step 2.

cargo test --bin ely_app: 120 passed, 0 failed, 2 ignored.
Hash collision probability remains ~1 in 2^64; AHash uses the same
keyspace as the previous SipHash13.
This commit is contained in:
2026-05-10 20:25:01 -04:00
parent 9711167560
commit 3f184ee941
4 changed files with 221 additions and 3 deletions
+1
View File
@@ -9,6 +9,7 @@ rust-version.workspace = true
live-site-smoke = []
[dependencies]
ahash = "0.8"
directories.workspace = true
ed25519-dalek.workspace = true
ely_browser_core = { path = "../ely_browser_core" }
+11 -3
View File
@@ -1,8 +1,8 @@
use std::cell::RefCell;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
use std::sync::Arc;
use ahash::AHasher;
use gpui::RenderImage;
use image::{ImageBuffer, Rgba};
use thiserror::Error;
@@ -17,7 +17,15 @@ thread_local! {
/// frame. The cache keys on a 64-bit hash of the raw bytes and
/// reuses the existing `Arc<RenderImage>` whenever the hash
/// matches, so steady-state idle pages no longer churn the GPUI
/// texture pool. Hash collisions are 1 in 2^64; if they ever
/// texture pool.
///
/// Uses `AHasher` instead of std's `DefaultHasher`. SipHash13
/// (default) tops out around ~1.5 GB/s; an 8 MB 1080p frame
/// hashes in ~5 ms on a modern CPU, which eats roughly 30 % of
/// the 16 ms scroll budget on every cache-miss frame. AHash
/// runs ~10 GB/s on the same hardware, dropping the per-frame
/// hash cost to ~0.8 ms and giving the scroll path back most of
/// that budget. Hash collisions remain ~1 in 2^64; if they ever
/// matter we'll trade in length + first/last 32 bytes as a
/// disambiguator before paying the full memcmp.
static LAST_FRAME_IMAGE: RefCell<Option<(u64, Arc<RenderImage>)>> =
@@ -201,7 +209,7 @@ pub(super) enum WebSurfaceError {
}
fn rgba_hash(bytes: &[u8]) -> u64 {
let mut hasher = DefaultHasher::new();
let mut hasher = AHasher::default();
hasher.write(bytes);
hasher.finish()
}