1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
//! Minimal Skia surface wrapper (Phase 1).
//!
//! Provides a small `SkiaSurface` helper for raster surfaces and a
//! placeholder for future window-backed surface creation.
#![allow(unused)]
#[cfg(feature = "skia-native")]
mod native {
use glow::HasContext;
use skia_safe as sk;
use std::path::Path;
use crate::viewport::Viewport;
pub struct SkiaSurface {
surface: sk::Surface,
pub viewport: Viewport,
/// Legacy aliases kept for backwards compat — kept in sync with viewport.
pub width: i32,
pub height: i32,
// Optional GPU context if available (kept for future extension).
//
// ONE field, not a `(DirectContext, SkiaGlContext)` pair. `GlDirectContext`
// owns both halves, and its own field order already guarantees the
// `DirectContext` is released while EGL is still alive. Holding the pair
// here as two `Option`s made this struct's correctness rest on its OWN
// declaration order matching a type in another file, with a comment
// asking readers not to reorder them. `GlDirectContext` is the unit that
// has to stay alive together; keeping it whole deletes the ordering
// question instead of documenting it.
pub(crate) _gpu: Option<crate::skia_gl::GlDirectContext>,
}
impl SkiaSurface {
/// Create a CPU raster SkiaSurface.
///
/// **This is deliberate — do not "optimize" it into a GPU context.**
///
/// This is the CPU raster path (`raster_n32_premul`, below), and it is the
/// *deterministic reference* the pixel proofs depend on. `velox-dom/tests/
/// todo_item_pixels.rs` and `velox-renderer/tests/caret_pixels.rs` assert
/// EXACT BYTE equality on rendered pixels; a GPU path with different
/// antialiasing, subpixel positioning, or resource-cache-dependent output
/// would silently invalidate ~949 tests that currently pass. Determinism was
/// chosen over the paint speedup.
///
/// There is a second, independent reason: the GPU path is not merely unused,
/// it is *unsafe to adopt as-is*. A `GrDirectContext`'s resource cache
/// defaults to **256 MB** and can occupy roughly twice that when full, and
/// this crate sets no limit (`grep 'resource_cache|set_resource_limit'` → no
/// hits). Any future GPU backend must set the limit explicitly and
/// implement device-lost handling before it is enabled.
///
/// The paint cost that remains on this path is **bandwidth, not allocation**
/// — three full-surface touches per frame (`canvas.clear` in `skia_render.rs`,
/// `read_pixels` into the reused `rgba` in `presenter.rs`, then a 1.92 MB
/// `copy_from_slice` into winit's buffer). That is a damage-limited-repaint
/// problem, not a buffer-reuse problem, and buffer reuse is already done.
///
/// See `docs/RENDERING.md` for the full decision record.
pub fn new_raster(width: i32, height: i32) -> Result<Self, String> {
let viewport = Viewport::from_i32(width, height, 1.0);
let w = viewport.physical_width_i32();
let h = viewport.physical_height_i32();
let surface = sk::surfaces::raster_n32_premul((w, h))
.ok_or_else(|| "skia: failed to create raster surface".to_string())?;
Ok(SkiaSurface {
surface,
viewport,
width: w,
height: h,
_gpu: None,
})
}
/// Return a reference to the canvas.
pub fn canvas(&mut self) -> &sk::Canvas {
// Prefer the mutable canvas accessor when available.
#[allow(deprecated)]
{
// `canvas_mut` is the explicit mutable accessor in some skia-safe versions.
// Fall back to calling `canvas()` on a mutable receiver if needed.
if false {
// no-op to keep fallback logic clear
}
}
#[cfg(all(feature = "skia-native", unix))]
if let Some(gpu) = &self._gpu {
let _ = gpu.gl().make_current();
}
self.surface.canvas()
}
/// Save the current surface snapshot to a PNG file (for debugging/tests).
pub fn save_png<P: AsRef<Path>>(&mut self, path: P) -> Result<(), String> {
let img = self.surface.image_snapshot();
#[allow(deprecated)]
let data = img
.encode_to_data(skia_safe::EncodedImageFormat::PNG)
.ok_or_else(|| "skia: failed to encode image".to_string())?;
std::fs::write(path, data.as_bytes()).map_err(|e| format!("write failed: {}", e))
}
/// Encode the current surface snapshot as PNG bytes.
pub fn encode_png(&mut self) -> Result<Vec<u8>, String> {
let img = self.surface.image_snapshot();
#[allow(deprecated)]
let data = img
.encode_to_data(skia_safe::EncodedImageFormat::PNG)
.ok_or_else(|| "skia: failed to encode image".to_string())?;
Ok(data.as_bytes().to_vec())
}
/// Present or flush any GPU work for this surface.
///
/// For GPU-backed surfaces this will flush and submit the `DirectContext`.
/// For raster surfaces this is a no-op.
pub fn present(&mut self) -> Result<(), String> {
#[cfg(all(feature = "skia-native", unix))]
if let Some(gpu) = &self._gpu {
let _ = gpu.gl().make_current();
}
if let Some(gpu) = &mut self._gpu {
gpu.dctx_mut().flush_and_submit();
}
Ok(())
}
/// Read RGBA pixels from the surface into the provided buffer.
pub fn read_pixels(
&mut self,
info: &sk::ImageInfo,
dst: &mut [u8],
row_bytes: usize,
src: (i32, i32),
) -> bool {
self.surface.read_pixels(info, dst, row_bytes, src)
}
/// Update the scale factor used for logical-to-physical mapping.
pub fn set_scale_factor(&mut self, scale_factor: f32) {
self.viewport.set_scale(scale_factor);
}
/// Current scale factor for logical-to-physical mapping.
pub fn scale_factor(&self) -> f32 {
self.viewport.scale
}
/// Resize the surface. Recreate a GPU-backed surface when a `DirectContext`
/// is available; otherwise recreate a CPU raster surface.
/// Clamps to max(1) before raster/glow, warns on error and keeps previous surface.
pub fn resize(&mut self, width: i32, height: i32) -> Result<(), String> {
let w = width.max(1);
let h = height.max(1);
// If we have a DirectContext, try to create a GPU-backed surface.
if let Some(gpu) = &mut self._gpu {
#[cfg(all(feature = "skia-native", unix))]
{
let _ = gpu.gl().make_current();
}
if let Some(new_surf) = create_gpu_surface_from_direct_context(gpu.dctx_mut(), w, h)
{
self.surface = new_surf;
self.viewport.set_physical_i32(w, h);
self.width = w;
self.height = h;
return Ok(());
}
// If GPU surface recreation failed, fall through to raster fallback.
}
// Raster fallback — attempt creation before mutating state.
match sk::surfaces::raster_n32_premul((w, h)) {
Some(surface) => {
self.surface = surface;
self.viewport.set_physical_i32(w, h);
self.width = w;
self.height = h;
Ok(())
}
None => {
log::warn!(
"SkiaSurface::resize failed to create raster surface {}x{} — keeping previous {}x{}",
w,
h,
self.width,
self.height
);
Err("skia: failed to create raster surface on resize".to_string())
}
}
}
}
/// Attempt to create a window-backed Skia surface from a raw-window-handle.
///
/// This function tries to create a native GL/EGL context for the provided
/// `HasWindowHandle` and, if successful, will attempt to create a
/// `DirectContext`. For Phase 1 we return a raster surface if creating a
/// GPU-backed surface is not yet supported.
pub fn create_window_surface_from_handle(
window: &impl raw_window_handle::HasRawWindowHandle,
width: i32,
height: i32,
) -> Result<SkiaSurface, String> {
let cw = width.max(1);
let ch = height.max(1);
// Try to create a native GL/EGL context using the helper in `skia_gl`.
match crate::skia_gl::create_context_from_winit(window) {
Ok(gl_ctx) => {
// `gl_ctx` is *consumed*: the returned `GlDirectContext` owns both
// the `DirectContext` and the EGL/GL context, so the two cannot be
// separated and the `DirectContext` cannot outlive its GL objects.
match gl_ctx.into_direct_context() {
Some(mut owned) => {
log::info!("DirectContext created (GPU path available)");
// Attempt to build a GPU-backed Skia surface from the DirectContext.
// If successful, return a SkiaSurface that owns both the DirectContext
// and the native GL context so they are kept alive for the lifetime
// of the surface.
if let Some(gpu_surf) =
create_gpu_surface_from_direct_context(owned.dctx_mut(), cw, ch)
{
let viewport = Viewport::from_i32(cw, ch, 1.0);
return Ok(SkiaSurface {
surface: gpu_surf,
viewport,
width: cw,
height: ch,
_gpu: Some(owned),
});
}
// Fallback to raster until the platform-specific path is implemented.
let viewport = Viewport::from_i32(cw, ch, 1.0);
let surface =
sk::surfaces::raster_n32_premul((cw, ch)).ok_or_else(|| {
"skia: failed to create raster fallback surface".to_string()
})?;
return Ok(SkiaSurface {
surface,
viewport,
width: cw,
height: ch,
_gpu: Some(owned),
});
}
None => {
log::warn!("Could not make DirectContext; falling back to raster");
}
}
}
Err(e) => {
log::error!("create_context_from_winit failed: {}", e);
}
}
// Fallback to CPU raster surface (already clamps)
SkiaSurface::new_raster(cw, ch)
}
pub use SkiaSurface as Surface;
/// Attempt to create a GPU-backed Skia surface from an existing DirectContext.
///
/// This is intentionally a stub: creating a `BackendRenderTarget` is
/// platform-specific (GL/EGL vs Metal vs D3D) and requires native handles.
/// Implementations should create a valid BackendRenderTarget and then call
/// `skia_safe::gpu::Surface::from_backend_render_target()` or similar.
fn create_gpu_surface_from_direct_context(
dctx: &mut sk::gpu::DirectContext,
width: i32,
height: i32,
) -> Option<sk::Surface> {
// Attempt to query the current GL framebuffer and wrap it as a
// Skia BackendRenderTarget. This is a best-effort implementation
// for EGL/GL on Unix. If anything fails, return None and the
// caller will fall back to a raster surface.
//
// SAFETY: `from_loader_function` only stores the loader for later
// symbol resolution, and `get_parameter_i32` reads the currently
// bound FBO — both require a current GL context on this thread. The
// sole caller (`create_window_surface_from_handle`, via
// `create_gpu_surface_from_direct_context`) runs after
// `into_direct_context` made the context current, so the context is
// current here. A wrong FBO id would only mis-target the wrap (which
// returns `None` and falls back to raster), never UB: no raw
// pointers escape this block.
unsafe {
// Create a glow context loader using EGL's get_proc_address.
let gl = glow::Context::from_loader_function(|s| egl::get_proc_address(s) as *const _);
// Query the currently bound FBO.
let fb_binding = gl.get_parameter_i32(glow::FRAMEBUFFER_BINDING) as u32;
// Choose a color format. We assume a standard RGBA8 buffer here.
let gl_format = glow::RGBA8;
let fb_info = sk::gpu::gl::FramebufferInfo {
fboid: fb_binding,
format: gl_format,
protected: sk::gpu::Protected::No,
};
// Create a BackendRenderTarget for GL.
let backend = sk::gpu::backend_render_targets::make_gl((width, height), 0, 8, fb_info);
// Create a Skia GPU-backed Surface from the backend render target.
let surface = sk::gpu::surfaces::wrap_backend_render_target(
dctx,
&backend,
sk::gpu::SurfaceOrigin::BottomLeft,
sk::ColorType::RGBA8888,
None,
None,
);
if surface.is_some() {
log::info!("created GPU-backed Surface (fbo={})", fb_binding);
} else {
log::warn!("Surface::from_backend_render_target returned None");
}
surface
}
}
}
#[cfg(not(feature = "skia-native"))]
mod nostub {
pub struct Surface {
_private: (),
}
pub fn create_window_surface(_w: i32, _h: i32) -> Result<Surface, String> {
Err("skia-native feature not enabled".into())
}
pub use Surface as SkiaSurface;
}
#[cfg(feature = "skia-native")]
pub use native::*;
#[cfg(not(feature = "skia-native"))]
pub use nostub::*;