penpot/render-wasm/src/render/gpu_state.rs
Alejandro Alonso 5dab689a6e 🐛 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.
2026-08-21 11:17:16 +02:00

235 lines
7.5 KiB
Rust

use crate::error::{Error, Result};
use skia_safe::gpu::{
self, ganesh::context_options::Enable, gl::FramebufferInfo, gl::TextureInfo, ContextOptions,
DirectContext,
};
use skia_safe::{self as skia, ISize};
const MIN_MAX_TEXTURE_SIZE: i32 = 512;
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)]
pub struct GpuState {
pub context: DirectContext,
framebuffer_info: FramebufferInfo,
}
impl GpuState {
pub fn try_new() -> Result<Self> {
let interface = gpu::gl::Interface::new_native().ok_or(Error::CriticalError(
"Failed to create GL interface".to_string(),
))?;
// We tweak some options to enhance performance.
let mut context_options = ContextOptions::default();
context_options.reduce_ops_task_splitting = Enable::No;
context_options.skip_gl_error_checks = Enable::Yes;
// context_options.runtime_program_cache_size = 1024;
// context_options.allow_multiple_glyph_cache_textures = Enable::Yes;
// context_options.allow_path_mask_caching = false;
let context = gpu::direct_contexts::make_gl(interface, Some(&context_options)).ok_or(
Error::CriticalError("Failed to create GL context".to_string()),
)?;
let framebuffer_info = {
let mut fboid: gl::types::GLint = 0;
unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) };
FramebufferInfo {
fboid: fboid.try_into().map_err(|_| {
Error::CriticalError("Failed to convert GL framebuffer ID to u32".to_string())
})?,
format: gpu::gl::Format::RGBA8.into(),
protected: gpu::Protected::No,
}
};
Ok(Self {
context,
framebuffer_info,
})
}
pub fn max_texture_size(&self) -> i32 {
self.context
.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 {
unsafe {
gl::DeleteTextures(1, &texture_id);
gl::GetError() == 0
}
}
fn create_gl_texture(&mut self, width: i32, height: i32) -> gl::types::GLuint {
let mut texture_id: gl::types::GLuint = 0;
unsafe {
gl::GenTextures(1, &mut texture_id);
gl::BindTexture(gl::TEXTURE_2D, texture_id);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as i32);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as i32);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32);
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32);
gl::TexImage2D(
gl::TEXTURE_2D,
0,
gl::RGBA8 as i32,
width,
height,
0,
gl::RGBA,
gl::UNSIGNED_BYTE,
std::ptr::null(),
);
}
texture_id
}
pub fn delete_surface(&mut self, surface: &mut skia::Surface) -> bool {
let Some(texture) = skia::gpu::surfaces::get_backend_texture(
surface,
skia_safe::surface::BackendHandleAccess::FlushRead,
) else {
return false;
};
let Some(texture_info) = gpu::backend_textures::get_gl_texture_info(&texture) else {
return false;
};
self.delete_gl_texture(texture_info.id)
}
pub fn create_surface_with_isize(
&mut self,
label: String,
size: ISize,
) -> Result<skia::Surface> {
self.create_surface_with_dimensions(label, size.width, size.height)
}
pub fn create_surface_with_dimensions(
&mut self,
label: String,
width: i32,
height: i32,
) -> Result<skia::Surface> {
let backend_texture = unsafe {
let texture_id = self.create_gl_texture(width, height);
let texture_info = TextureInfo {
target: gl::TEXTURE_2D,
id: texture_id,
format: gl::RGBA8,
protected: skia::gpu::Protected::No,
};
gpu::backend_textures::make_gl((width, height), gpu::Mipmapped::No, texture_info, label)
};
let surface = gpu::surfaces::wrap_backend_texture(
&mut self.context,
&backend_texture,
gpu::SurfaceOrigin::BottomLeft,
None,
skia::ColorType::RGBA8888,
None,
None,
)
.ok_or(Error::CriticalError(
"Failed to create Skia surface".to_string(),
))?;
Ok(surface)
}
/// Create a Skia surface that will be used for rendering.
pub fn create_target_surface(&mut self, width: i32, height: i32) -> Result<skia::Surface> {
let backend_render_target =
gpu::backend_render_targets::make_gl((width, height), 1, 8, self.framebuffer_info);
let surface = gpu::surfaces::wrap_backend_render_target(
&mut self.context,
&backend_render_target,
gpu::SurfaceOrigin::BottomLeft,
skia::ColorType::RGBA8888,
None,
None,
)
.ok_or(Error::CriticalError(
"Failed to create Skia surface".to_string(),
))?;
Ok(surface)
}
#[allow(dead_code)]
pub fn create_surface_from_texture(
&mut self,
width: i32,
height: i32,
texture_id: u32,
) -> skia::Surface {
let texture_info = TextureInfo {
target: gl::TEXTURE_2D,
id: texture_id,
format: gl::RGBA8,
protected: skia::gpu::Protected::No,
};
let backend_texture = unsafe {
gpu::backend_textures::make_gl(
(width, height),
gpu::Mipmapped::No,
texture_info,
String::from("export_texture"),
)
};
gpu::surfaces::wrap_backend_texture(
&mut self.context,
&backend_texture,
gpu::SurfaceOrigin::BottomLeft,
None,
skia::ColorType::RGBA8888,
None,
None,
)
.unwrap()
}
}