mirror of
https://github.com/penpot/penpot.git
synced 2026-08-24 21:59:02 +00:00
🐛 Pack tile atlas and clamp HiDPI surfaces under GPU limits
HQ tiles are 512px and the atlas stays at 4096² (64 full-size slots). Browser zoom plus a forced ?dpr= can need more visible tiles than that, and a framebuffer larger than the GPU allows. Pack interest tiles into smaller atlas cells, blit at 512 then scale, and inset Linear samples so seams do not bleed. Clamp the canvas backing store and DPR together, wrap Skia at the real drawingBuffer size, and wait one frame after DPR changes so CSS client size and overlays stay aligned.
This commit is contained in:
parent
689d506788
commit
5dab689a6e
@ -2177,15 +2177,31 @@
|
|||||||
(when (wasm/live?)
|
(when (wasm/live?)
|
||||||
(h/call wasm/internal-module "_set_render_options" (debug-flags) new-dpr)))
|
(h/call wasm/internal-module "_set_render_options" (debug-flags) new-dpr)))
|
||||||
|
|
||||||
|
(def ^:private max-surface-size
|
||||||
|
;; Must match `gpu_state::MAX_SURFACE_SIZE`.
|
||||||
|
8192)
|
||||||
|
|
||||||
|
(defn- clamp-physical-size
|
||||||
|
"Clamp physical pixel dimensions before assigning `canvas.width/height`.
|
||||||
|
Rust `resize` applies the same cap and syncs the effective DPR from the
|
||||||
|
real drawing buffer."
|
||||||
|
[w h]
|
||||||
|
(let [w (mth/max 1 w)
|
||||||
|
h (mth/max 1 h)
|
||||||
|
scale (mth/min 1 (/ max-surface-size w) (/ max-surface-size h))]
|
||||||
|
[(mth/max 1 (mth/floor (* scale w)))
|
||||||
|
(mth/max 1 (mth/floor (* scale h)))]))
|
||||||
|
|
||||||
(defn resize-offscreen-canvas!
|
(defn resize-offscreen-canvas!
|
||||||
"Resize a persistent OffscreenCanvas to new physical-pixel dimensions and
|
"Resize a persistent OffscreenCanvas to new physical-pixel dimensions and
|
||||||
update the WASM render surfaces accordingly (via `_resize_viewbox`). The
|
update the WASM render surfaces accordingly (via `_resize_viewbox`). The
|
||||||
design state (shape pool) is preserved so `set-objects` is not needed again."
|
design state (shape pool) is preserved so `set-objects` is not needed again."
|
||||||
[canvas new-physical-w new-physical-h]
|
[canvas new-physical-w new-physical-h]
|
||||||
(when (wasm/live?)
|
(when (wasm/live?)
|
||||||
(let [dpr (get-dpr)]
|
(let [dpr (get-dpr)
|
||||||
(set! (.-width canvas) new-physical-w)
|
[pw ph] (clamp-physical-size new-physical-w new-physical-h)]
|
||||||
(set! (.-height canvas) new-physical-h)
|
(set! (.-width canvas) pw)
|
||||||
|
(set! (.-height canvas) ph)
|
||||||
(set-render-options! dpr)
|
(set-render-options! dpr)
|
||||||
(resize-viewbox (/ new-physical-w dpr) (/ new-physical-h dpr)))))
|
(resize-viewbox (/ new-physical-w dpr) (/ new-physical-h dpr)))))
|
||||||
|
|
||||||
@ -2220,9 +2236,14 @@
|
|||||||
(resize-canvas! canvas (get-dpr)))
|
(resize-canvas! canvas (get-dpr)))
|
||||||
([canvas new-dpr]
|
([canvas new-dpr]
|
||||||
(when (wasm/live?)
|
(when (wasm/live?)
|
||||||
(let [[css-w css-h] (canvas-css-size canvas new-dpr)]
|
(let [[css-w css-h] (canvas-css-size canvas new-dpr)
|
||||||
(set! (.-width ^js canvas) (* new-dpr css-w))
|
css-w (mth/max 1 css-w)
|
||||||
(set! (.-height ^js canvas) (* new-dpr css-h))
|
css-h (mth/max 1 css-h)
|
||||||
|
[phys-w phys-h] (clamp-physical-size
|
||||||
|
(mth/floor (* css-w new-dpr))
|
||||||
|
(mth/floor (* css-h new-dpr)))]
|
||||||
|
(set! (.-width ^js canvas) phys-w)
|
||||||
|
(set! (.-height ^js canvas) phys-h)
|
||||||
(set-render-options! new-dpr)
|
(set-render-options! new-dpr)
|
||||||
(resize-viewbox css-w css-h)))))
|
(resize-viewbox css-w css-h)))))
|
||||||
|
|
||||||
|
|||||||
@ -57,7 +57,7 @@ pub extern "C" fn set_viewport_interest_area_threshold(
|
|||||||
viewport_interest_area_threshold: i32,
|
viewport_interest_area_threshold: i32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let render_state = get_render_state();
|
let render_state = get_render_state();
|
||||||
render_state.set_viewport_interest_area_threshold(viewport_interest_area_threshold);
|
render_state.set_viewport_interest_area_threshold(viewport_interest_area_threshold)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,10 @@ use skia_safe::{self as skia, ISize};
|
|||||||
|
|
||||||
const MIN_MAX_TEXTURE_SIZE: i32 = 512;
|
const MIN_MAX_TEXTURE_SIZE: i32 = 512;
|
||||||
const MAX_MAX_TEXTURE_SIZE: i32 = 4096;
|
const MAX_MAX_TEXTURE_SIZE: i32 = 4096;
|
||||||
|
/// Cap for the canvas framebuffer / backbuffer (not the tile atlas).
|
||||||
|
/// Larger than a typical viewport at DPR 2 but below sizes that would exceed
|
||||||
|
/// GPU limits when CSS dimensions are very large.
|
||||||
|
pub const MAX_SURFACE_SIZE: i32 = 8192;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct GpuState {
|
pub struct GpuState {
|
||||||
@ -57,6 +61,33 @@ impl GpuState {
|
|||||||
.clamp(MIN_MAX_TEXTURE_SIZE, MAX_MAX_TEXTURE_SIZE)
|
.clamp(MIN_MAX_TEXTURE_SIZE, MAX_MAX_TEXTURE_SIZE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn max_surface_size(&self) -> i32 {
|
||||||
|
self.context
|
||||||
|
.max_texture_size()
|
||||||
|
.clamp(MIN_MAX_TEXTURE_SIZE, MAX_SURFACE_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actual default-framebuffer size after the canvas backing store is set.
|
||||||
|
/// Browsers may allocate a smaller `drawingBuffer` than `canvas.width`;
|
||||||
|
/// wrapping Skia at the requested size then shifts content (GL origin is
|
||||||
|
/// bottom-left). Native builds have no canvas; return `None`.
|
||||||
|
pub fn drawing_buffer_size(&self) -> Option<(i32, i32)> {
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
let w = crate::run_script_int!(
|
||||||
|
"(typeof GLctx!=='undefined'&&GLctx)?GLctx.drawingBufferWidth:0"
|
||||||
|
);
|
||||||
|
let h = crate::run_script_int!(
|
||||||
|
"(typeof GLctx!=='undefined'&&GLctx)?GLctx.drawingBufferHeight:0"
|
||||||
|
);
|
||||||
|
if w > 0 && h > 0 {
|
||||||
|
return Some((w, h));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = self;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn delete_gl_texture(&mut self, texture_id: gl::types::GLuint) -> bool {
|
fn delete_gl_texture(&mut self, texture_id: gl::types::GLuint) -> bool {
|
||||||
unsafe {
|
unsafe {
|
||||||
gl::DeleteTextures(1, &texture_id);
|
gl::DeleteTextures(1, &texture_id);
|
||||||
|
|||||||
@ -503,8 +503,7 @@ impl Surfaces {
|
|||||||
let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height)?;
|
let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height)?;
|
||||||
let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height)?;
|
let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height)?;
|
||||||
|
|
||||||
// 512, why not?
|
let tiles = TileTextureCache::new(tile_atlas.width(), tile_atlas.height());
|
||||||
let tiles = TileTextureCache::new(tile_atlas.width(), 512);
|
|
||||||
let atlas = DocAtlas::try_new()?;
|
let atlas = DocAtlas::try_new()?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
target,
|
target,
|
||||||
@ -536,6 +535,20 @@ impl Surfaces {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pack `needed_slots` into the existing 4096 atlas by shrinking the
|
||||||
|
/// physical cell size. No-op when the layout already fits.
|
||||||
|
pub fn ensure_tile_atlas_layout(&mut self, needed_slots: usize) {
|
||||||
|
let atlas_px = self.tile_atlas.width().min(self.tile_atlas.height());
|
||||||
|
let slot = tiles::tile_atlas_slot_size(needed_slots, atlas_px);
|
||||||
|
if slot == self.tiles.slot_size() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.tiles
|
||||||
|
.repack(self.tile_atlas.width(), self.tile_atlas.height(), slot);
|
||||||
|
self.tile_atlas.canvas().clear(skia::Color::TRANSPARENT);
|
||||||
|
self.tile_atlas_image = None;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_dpr(&mut self, dpr: f32) {
|
pub fn set_dpr(&mut self, dpr: f32) {
|
||||||
self.dpr = dpr;
|
self.dpr = dpr;
|
||||||
}
|
}
|
||||||
@ -544,6 +557,14 @@ impl Surfaces {
|
|||||||
self.tiles.clear();
|
self.tiles.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tile_atlas_sampling(&self) -> skia::SamplingOptions {
|
||||||
|
if self.tiles.slot_size() < TILE_SIZE {
|
||||||
|
skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::None)
|
||||||
|
} else {
|
||||||
|
self.atlas_sampling_options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn draw_tile_atlas_to_backbuffer(
|
pub fn draw_tile_atlas_to_backbuffer(
|
||||||
&mut self,
|
&mut self,
|
||||||
viewbox: &Viewbox,
|
viewbox: &Viewbox,
|
||||||
@ -558,6 +579,7 @@ impl Surfaces {
|
|||||||
let Some(atlas_image) = self.tile_atlas_image.as_ref() else {
|
let Some(atlas_image) = self.tile_atlas_image.as_ref() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let sampling = self.tile_atlas_sampling();
|
||||||
let canvas = self.backbuffer.canvas();
|
let canvas = self.backbuffer.canvas();
|
||||||
canvas.clear(background);
|
canvas.clear(background);
|
||||||
canvas.draw_atlas(
|
canvas.draw_atlas(
|
||||||
@ -566,7 +588,7 @@ impl Surfaces {
|
|||||||
&self.tiles.textures,
|
&self.tiles.textures,
|
||||||
None,
|
None,
|
||||||
skia::BlendMode::SrcOver,
|
skia::BlendMode::SrcOver,
|
||||||
self.atlas_sampling_options,
|
sampling,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
@ -643,6 +665,7 @@ impl Surfaces {
|
|||||||
let Some(atlas_image) = self.tile_atlas_image.as_ref() else {
|
let Some(atlas_image) = self.tile_atlas_image.as_ref() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let sampling = self.tile_atlas_sampling();
|
||||||
|
|
||||||
let canvas = self.backbuffer.canvas();
|
let canvas = self.backbuffer.canvas();
|
||||||
canvas.save();
|
canvas.save();
|
||||||
@ -654,7 +677,7 @@ impl Surfaces {
|
|||||||
&batch.textures,
|
&batch.textures,
|
||||||
None,
|
None,
|
||||||
skia::BlendMode::SrcOver,
|
skia::BlendMode::SrcOver,
|
||||||
self.atlas_sampling_options,
|
sampling,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
@ -1515,21 +1538,24 @@ pub struct TileAtlasTextureProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TileAtlasTextureProvider {
|
impl TileAtlasTextureProvider {
|
||||||
pub fn new(texture_size: i32, tile_size: i32) -> Self {
|
pub fn new(texture_width: i32, texture_height: i32, tile_size: i32) -> Self {
|
||||||
let side = texture_size / tile_size;
|
let cols = texture_width / tile_size;
|
||||||
let length = side * side;
|
let rows = texture_height / tile_size;
|
||||||
let mut rects = Vec::with_capacity(length as usize);
|
let length = (cols * rows) as usize;
|
||||||
for i in 0..length {
|
let mut rects = Vec::with_capacity(length);
|
||||||
let left = (i % side) as f32 * tile_size as f32;
|
for row in 0..rows {
|
||||||
let top = (i / side) as f32 * tile_size as f32;
|
for col in 0..cols {
|
||||||
let right = left + tile_size as f32;
|
let left = col as f32 * tile_size as f32;
|
||||||
let bottom = top + tile_size as f32;
|
let top = row as f32 * tile_size as f32;
|
||||||
rects.push(Rect::new(left, top, right, bottom));
|
let right = left + tile_size as f32;
|
||||||
|
let bottom = top + tile_size as f32;
|
||||||
|
rects.push(Rect::new(left, top, right, bottom));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
index: 0,
|
index: 0,
|
||||||
length: length as usize,
|
length,
|
||||||
in_use: vec![false; length as usize],
|
in_use: vec![false; length],
|
||||||
rects,
|
rects,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1563,6 +1589,7 @@ impl TileAtlasTextureProvider {
|
|||||||
|
|
||||||
pub struct TileTextureCache {
|
pub struct TileTextureCache {
|
||||||
tile_size: f32,
|
tile_size: f32,
|
||||||
|
slot_size: i32,
|
||||||
is_updated: bool,
|
is_updated: bool,
|
||||||
provider: TileAtlasTextureProvider,
|
provider: TileAtlasTextureProvider,
|
||||||
transforms: Vec<skia::RSXform>,
|
transforms: Vec<skia::RSXform>,
|
||||||
@ -1583,11 +1610,13 @@ impl AtlasDrawBatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TileTextureCache {
|
impl TileTextureCache {
|
||||||
pub fn new(texture_size: i32, capacity: usize) -> Self {
|
pub fn new(texture_width: i32, texture_height: i32) -> Self {
|
||||||
|
let capacity = ((texture_width / TILE_SIZE) * (texture_height / TILE_SIZE)) as usize;
|
||||||
Self {
|
Self {
|
||||||
tile_size: tiles::TILE_SIZE,
|
tile_size: tiles::TILE_SIZE,
|
||||||
|
slot_size: TILE_SIZE,
|
||||||
is_updated: false,
|
is_updated: false,
|
||||||
provider: TileAtlasTextureProvider::new(texture_size, TILE_SIZE),
|
provider: TileAtlasTextureProvider::new(texture_width, texture_height, TILE_SIZE),
|
||||||
transforms: Vec::with_capacity(capacity),
|
transforms: Vec::with_capacity(capacity),
|
||||||
textures: Vec::with_capacity(capacity),
|
textures: Vec::with_capacity(capacity),
|
||||||
grid: HashMap::with_capacity(capacity),
|
grid: HashMap::with_capacity(capacity),
|
||||||
@ -1595,6 +1624,39 @@ impl TileTextureCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn slot_size(&self) -> i32 {
|
||||||
|
self.slot_size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dest_scale(&self) -> f32 {
|
||||||
|
tiles::tile_atlas_compose_scale(self.slot_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compose_src_rect(&self, rect: Rect) -> Rect {
|
||||||
|
if self.slot_size < TILE_SIZE {
|
||||||
|
let inset = tiles::TILE_ATLAS_SAMPLE_INSET;
|
||||||
|
Rect::new(
|
||||||
|
rect.left + inset,
|
||||||
|
rect.top + inset,
|
||||||
|
rect.right - inset,
|
||||||
|
rect.bottom - inset,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
rect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn repack(&mut self, texture_width: i32, texture_height: i32, slot_size: i32) {
|
||||||
|
let capacity = ((texture_width / slot_size) * (texture_height / slot_size)) as usize;
|
||||||
|
self.slot_size = slot_size;
|
||||||
|
self.is_updated = true;
|
||||||
|
self.provider = TileAtlasTextureProvider::new(texture_width, texture_height, slot_size);
|
||||||
|
self.transforms = Vec::with_capacity(capacity);
|
||||||
|
self.textures = Vec::with_capacity(capacity);
|
||||||
|
self.grid = HashMap::with_capacity(capacity);
|
||||||
|
self.removed = HashSet::with_capacity(capacity);
|
||||||
|
}
|
||||||
|
|
||||||
fn gc(&mut self) {
|
fn gc(&mut self) {
|
||||||
// Make a real remove
|
// Make a real remove
|
||||||
for tile in self.removed.iter() {
|
for tile in self.removed.iter() {
|
||||||
@ -1634,10 +1696,11 @@ impl TileTextureCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, viewbox: &Viewbox, tile_viewbox: &TileViewbox) {
|
pub fn update(&mut self, viewbox: &Viewbox, tile_viewbox: &TileViewbox) {
|
||||||
|
let dest_scale = self.dest_scale();
|
||||||
if self.transforms.len() != tile_viewbox.visible_rect.len() as usize {
|
if self.transforms.len() != tile_viewbox.visible_rect.len() as usize {
|
||||||
self.transforms.resize(
|
self.transforms.resize(
|
||||||
tile_viewbox.visible_rect.len() as usize,
|
tile_viewbox.visible_rect.len() as usize,
|
||||||
skia::RSXform::new(1.0, 0.0, Point::default()),
|
skia::RSXform::new(dest_scale, 0.0, Point::default()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1666,16 +1729,18 @@ impl TileTextureCache {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.transforms[index].tx = x as f32 * self.tile_size - offset.x;
|
self.transforms[index] = skia::RSXform::new(
|
||||||
self.transforms[index].ty = y as f32 * self.tile_size - offset.y;
|
dest_scale,
|
||||||
|
0.0,
|
||||||
self.textures[index].set_ltrb(
|
(
|
||||||
tile_ref.rect.left,
|
(x as f32 * self.tile_size - offset.x).round(),
|
||||||
tile_ref.rect.top,
|
(y as f32 * self.tile_size - offset.y).round(),
|
||||||
tile_ref.rect.right,
|
),
|
||||||
tile_ref.rect.bottom,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let src = self.compose_src_rect(tile_ref.rect);
|
||||||
|
self.textures[index].set_ltrb(src.left, src.top, src.right, src.bottom);
|
||||||
|
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1713,12 +1778,13 @@ impl TileTextureCache {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let scos = doc_rect.width() * s / self.tile_size;
|
let src = self.compose_src_rect(tile_ref.rect);
|
||||||
let tx = (doc_rect.left + viewbox.pan.x) * s;
|
let scos = doc_rect.width() * s / src.width();
|
||||||
let ty = (doc_rect.top + viewbox.pan.y) * s;
|
let tx = ((doc_rect.left + viewbox.pan.x) * s).round();
|
||||||
|
let ty = ((doc_rect.top + viewbox.pan.y) * s).round();
|
||||||
|
|
||||||
transforms.push(skia::RSXform::new(scos, 0.0, (tx, ty)));
|
transforms.push(skia::RSXform::new(scos, 0.0, (tx, ty)));
|
||||||
textures.push(tile_ref.rect);
|
textures.push(src);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1737,12 +1803,13 @@ impl TileTextureCache {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tx = (doc_rect.left + viewbox.pan.x) * s;
|
let src = self.compose_src_rect(tile_ref.rect);
|
||||||
let ty = (doc_rect.top + viewbox.pan.y) * s;
|
let tx = ((doc_rect.left + viewbox.pan.x) * s).round();
|
||||||
let scos = doc_rect.width() * s / self.tile_size;
|
let ty = ((doc_rect.top + viewbox.pan.y) * s).round();
|
||||||
|
let scos = doc_rect.width() * s / src.width();
|
||||||
|
|
||||||
transforms.push(skia::RSXform::new(scos, 0.0, (tx, ty)));
|
transforms.push(skia::RSXform::new(scos, 0.0, (tx, ty)));
|
||||||
textures.push(tile_ref.rect);
|
textures.push(src);
|
||||||
}
|
}
|
||||||
|
|
||||||
AtlasDrawBatch {
|
AtlasDrawBatch {
|
||||||
@ -1769,7 +1836,10 @@ impl TileTextureCache {
|
|||||||
let Some(tile_ref) = self.provider.allocate() else {
|
let Some(tile_ref) = self.provider.allocate() else {
|
||||||
panic!("Tile texture allocation failed {}:{}", tile.0, tile.1);
|
panic!("Tile texture allocation failed {}:{}", tile.0, tile.1);
|
||||||
};
|
};
|
||||||
|
self.insert(tile, tile_ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert(&mut self, tile: &Tile, tile_ref: TileAtlasTextureRef) -> TileAtlasTextureRef {
|
||||||
self.grid.insert(*tile, tile_ref.clone());
|
self.grid.insert(*tile, tile_ref.clone());
|
||||||
|
|
||||||
if self.removed.contains(tile) {
|
if self.removed.contains(tile) {
|
||||||
@ -1777,7 +1847,7 @@ impl TileTextureCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.is_updated = true;
|
self.is_updated = true;
|
||||||
tile_ref.clone()
|
tile_ref
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&mut self, tile: Tile) -> Option<&TileAtlasTextureRef> {
|
pub fn get(&mut self, tile: Tile) -> Option<&TileAtlasTextureRef> {
|
||||||
|
|||||||
@ -258,6 +258,35 @@ pub fn get_tile_rect(tile: Tile, scale: f32) -> skia::Rect {
|
|||||||
skia::Rect::from_xywh(tx, ty, ts, ts)
|
skia::Rect::from_xywh(tx, ty, ts, ts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Physical atlas cell size so `needed_slots` fit in a square `atlas_px`
|
||||||
|
/// texture. Never larger than `TILE_SIZE` (tiles are stored 1:1 when they
|
||||||
|
/// fit). Smaller cells mean more slots, scaled down on blit into the atlas.
|
||||||
|
pub fn tile_atlas_slot_size(needed_slots: usize, atlas_px: i32) -> i32 {
|
||||||
|
const MIN_SLOT: i32 = 64;
|
||||||
|
let needed = needed_slots.max(1);
|
||||||
|
let side = (needed as f64).sqrt().ceil() as i32;
|
||||||
|
let side = side.max(1);
|
||||||
|
(atlas_px / side).clamp(MIN_SLOT, TILE_SIZE as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inset (texels) applied when sampling a packed atlas slot with Linear
|
||||||
|
/// filtering, so upsample kernels do not bleed into the neighboring cell.
|
||||||
|
pub const TILE_ATLAS_SAMPLE_INSET: f32 = 1.0;
|
||||||
|
|
||||||
|
/// Source size inside a packed slot after the Linear-filter inset.
|
||||||
|
pub fn tile_atlas_compose_src_size(slot_size: i32) -> f32 {
|
||||||
|
if slot_size < TILE_SIZE as i32 {
|
||||||
|
(slot_size as f32 - 2.0 * TILE_ATLAS_SAMPLE_INSET).max(1.0)
|
||||||
|
} else {
|
||||||
|
slot_size as f32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `draw_atlas` scale so the destination sprite stays `TILE_SIZE` after inset.
|
||||||
|
pub fn tile_atlas_compose_scale(slot_size: i32) -> f32 {
|
||||||
|
TILE_SIZE / tile_atlas_compose_src_size(slot_size)
|
||||||
|
}
|
||||||
|
|
||||||
// This structure is useful to keep all the shape uuids by shape id.
|
// This structure is useful to keep all the shape uuids by shape id.
|
||||||
pub struct TileHashMap {
|
pub struct TileHashMap {
|
||||||
grid: HashMap<Tile, HashSet<Uuid>>,
|
grid: HashMap<Tile, HashSet<Uuid>>,
|
||||||
@ -404,3 +433,36 @@ impl PendingTiles {
|
|||||||
self.list.pop()
|
self.list.pop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn atlas_slot_is_full_size_when_tiles_fit() {
|
||||||
|
assert_eq!(tile_atlas_slot_size(64, 4096), 512);
|
||||||
|
assert_eq!(tile_atlas_slot_size(1, 4096), 512);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn atlas_slot_shrinks_to_pack_interest_tiles() {
|
||||||
|
// 150 slots → 13×13 grid, 4096/13 = 315.
|
||||||
|
assert_eq!(tile_atlas_slot_size(150, 4096), 315);
|
||||||
|
let side = 4096 / 315;
|
||||||
|
assert!(side * side >= 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn atlas_compose_scale_is_one_at_full_slot() {
|
||||||
|
assert_eq!(tile_atlas_compose_scale(512), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn atlas_compose_scale_keeps_dest_tile_size_when_packed() {
|
||||||
|
let slot = 315;
|
||||||
|
let scale = tile_atlas_compose_scale(slot);
|
||||||
|
let src = tile_atlas_compose_src_size(slot);
|
||||||
|
assert!((scale * src - TILE_SIZE).abs() < 1e-4);
|
||||||
|
assert!(src < slot as f32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -94,3 +94,59 @@ impl Viewbox {
|
|||||||
matrix
|
matrix
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scale `dpr` down so `floor(css * dpr)` fits in `max_dim` on both axes.
|
||||||
|
/// Used when a large viewport combined with a high DPR would exceed the GPU
|
||||||
|
/// (or our surface cap) on either axis.
|
||||||
|
pub fn clamp_dpr_for_surface(css_w: f32, css_h: f32, dpr: f32, max_dim: i32) -> f32 {
|
||||||
|
let css_w = css_w.max(1.0);
|
||||||
|
let css_h = css_h.max(1.0);
|
||||||
|
let dpr = dpr.max(0.0);
|
||||||
|
let max_dim = max_dim.max(1) as f32;
|
||||||
|
let raw_w = (css_w * dpr).floor().max(1.0);
|
||||||
|
let raw_h = (css_h * dpr).floor().max(1.0);
|
||||||
|
let scale = (max_dim / raw_w).min(max_dim / raw_h).min(1.0);
|
||||||
|
dpr * scale
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::clamp_dpr_for_surface;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clamp_dpr_keeps_hidpi_viewport_under_cap() {
|
||||||
|
let dpr = clamp_dpr_for_surface(2560.0, 1440.0, 2.0, 8192);
|
||||||
|
assert!((dpr - 2.0).abs() < 1e-5);
|
||||||
|
assert!((2560.0 * dpr).floor() <= 8192.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clamp_dpr_caps_very_large_viewport_at_dpr2() {
|
||||||
|
// 10240×5760 CSS at DPR 2 → 20480 px unclamped on the long edge.
|
||||||
|
let dpr = clamp_dpr_for_surface(10240.0, 5760.0, 2.0, 8192);
|
||||||
|
assert!((10240.0 * dpr).floor() <= 8192.0);
|
||||||
|
assert!((5760.0 * dpr).floor() <= 8192.0);
|
||||||
|
assert!(dpr < 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clamp_dpr_caps_large_viewport_at_dpr2() {
|
||||||
|
let dpr = clamp_dpr_for_surface(5120.0, 2880.0, 2.0, 8192);
|
||||||
|
assert!((5120.0 * dpr).floor() <= 8192.0);
|
||||||
|
assert!(dpr < 2.0);
|
||||||
|
assert!(dpr > 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clamp_dpr_physical_size_is_floor_of_css_times_dpr() {
|
||||||
|
let css_w = 5120.0;
|
||||||
|
let css_h = 2880.0;
|
||||||
|
let dpr = clamp_dpr_for_surface(css_w, css_h, 2.0, 8192);
|
||||||
|
let phys_w = (css_w * dpr).floor();
|
||||||
|
let phys_h = (css_h * dpr).floor();
|
||||||
|
assert!(phys_w <= 8192.0);
|
||||||
|
assert!(phys_h <= 8192.0);
|
||||||
|
assert!((css_w * dpr - phys_w).abs() < 1.0);
|
||||||
|
assert!((css_h * dpr - phys_h).abs() < 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user