///|
pub struct SkiaPixelFrame {
priv width : Int
priv height : Int
priv row_bytes : Int
priv scale_factor : Double
priv pixels : Bytes
} derive(Eq, Debug)
///|
pub struct SkiaPresentTarget {
priv description : String
priv present : (SkiaPixelFrame) -> Bool
}
///|
/// Discriminator for which present path a `SkiaRasterRenderer` is wired to.
///
/// - `CpuPixelFrame`: the renderer reads pixels back via `Surface::read_pixels`
/// and hands a `SkiaPixelFrame` to the platform presenter. This is the only
/// path wired today on every platform and remains the default/test fallback
/// per ADR 0006.
/// - `HostGpuSurface`: the renderer is bound to a `HostGpuPresentTarget`. The
/// platform presenter owns a window-backed GPU surface (Metal drawable,
/// Vulkan swapchain, EGL surface, D3D12 swapchain) and Skia flushes the
/// drawable directly. `read_pixels` and the second CPU copy are skipped.
pub(all) enum SkiaPresentKind {
CpuPixelFrame
HostGpuSurface
} derive(Eq, Debug, ToJson)
///|
/// Present target for the direct host-GPU-surface route. The presenter takes a
/// flushed `@skia_native.Surface` and lets the platform present its underlying
/// drawable (Metal `CAMetalDrawable`, `VkSwapchainKHR` image, `EGLSurface`
/// backbuffer, or D3D12 swapchain backbuffer). No `SkiaPixelFrame`, no `Bytes`,
/// no `CGImage`/`UIImage`/`NSImage`/`Bitmap`/`PixelMap` intermediate.
///
/// `HostGpuPresentTarget` is a sibling of `SkiaPresentTarget`; both are
/// accepted by `create_with_present_target_and_route`. A renderer is bound to
/// exactly one of them. When `gpu_target = Some(_)`, `target_kind()` returns
/// `HostGpuSurface` and the render loop bypasses `read_frame`.
///
/// `create_window_surface` is invoked lazily immediately before the first
/// render, after the native event loop can provide a drawable, and again on
/// each resize after attachment. It receives the `GpuContext` the renderer
/// created internally (via `create_gpu_context`) and the current surface
/// metrics, and returns a window-backed `Surface` (e.g. one wrapping a Metal
/// `CAMetalDrawable`'s `MTLTexture`). When it returns `None`, the renderer
/// raises `SurfaceUnavailable`. The constructor's offscreen GPU surface is a
/// typed placeholder only; no commands are recorded into it, so the first
/// rendered frame still goes directly to the host drawable with no readback.
///
/// `present_gpu` is invoked each frame after `flush_and_submit`. It receives
/// the same `GpuContext` and the flushed `Surface`. It returns the optional
/// `Surface` to use for the *next* frame. GPU backends whose Skia `Surface` is
/// bound to a per-frame drawable (Metal `CAMetalDrawable`, Vulkan swapchain
/// image) return a fresh `Surface`; backends that reuse the same `Surface`
/// (e.g. a long-lived `EGLSurface`/D3D12 swapchain backbuffer wrapped via
/// `SkSurfaces::WrapBackendSurface`) return `None` to keep the current one.
/// Returning `Some(surface)` causes the renderer to swap `self.surface` to
/// the returned value before the next frame.
pub struct HostGpuPresentTarget {
priv description : String
priv create_window_surface : (@skia_native.GpuContext?, @core.SurfaceMetrics) -> @skia_native.Surface?
priv present_gpu : (@skia_native.GpuContext?, @skia_native.Surface) -> HostGpuPresentOutcome
}
///|
/// Outcome of a `HostGpuPresentTarget::present_gpu` call.
///
/// - `succeeded`: whether the platform presented the drawable successfully.
/// - `next_surface`: `Some(surface)` to swap the renderer's surface for the
/// next frame (per-drawable GPU backends); `None` to keep the current
/// surface (long-lived GPU backends).
pub(all) struct HostGpuPresentOutcome {
priv succeeded : Bool
priv next_surface : @skia_native.Surface?
}
///|
pub fn HostGpuPresentOutcome::new(
succeeded~ : Bool,
next_surface? : @skia_native.Surface? = None,
) -> HostGpuPresentOutcome {
{ succeeded, next_surface }
}
///|
pub fn HostGpuPresentOutcome::succeeded(self : HostGpuPresentOutcome) -> Bool {
self.succeeded
}
///|
pub fn HostGpuPresentOutcome::next_surface(
self : HostGpuPresentOutcome,
) -> @skia_native.Surface? {
self.next_surface
}
///|
pub fn HostGpuPresentTarget::new(
description~ : String,
create_window_surface~ : (@skia_native.GpuContext?, @core.SurfaceMetrics) -> @skia_native.Surface?,
present_gpu~ : (@skia_native.GpuContext?, @skia_native.Surface) -> HostGpuPresentOutcome,
) -> HostGpuPresentTarget {
{ description, create_window_surface, present_gpu }
}
///|
pub fn HostGpuPresentTarget::description(self : HostGpuPresentTarget) -> String {
self.description
}
///|
/// `SkiaSurfaceRoute` is renderer-local because it names concrete graphics
/// APIs that must not cross the root `moui/render` boundary.
///|
priv struct SkiaRasterSurfacePreflight {
physical_width : Int
physical_height : Int
target_ready : Bool
frame_ready : Bool
finalization_ready : Bool
submission_ready : Bool
resource_count : Int
cacheable_resource_count : Int
uncacheable_resource_count : Int
gpu_backed_resource_count : Int
surface_resource_count : Int
byte_size : Int64
}
///|
fn SkiaRasterSurfacePreflight::summary(
self : SkiaRasterSurfacePreflight,
) -> String {
let dimensions_ready = self.physical_width > 0 && self.physical_height > 0
let ready = dimensions_ready &&
self.target_ready &&
self.frame_ready &&
self.finalization_ready &&
self.submission_ready &&
self.resource_count ==
self.cacheable_resource_count + self.uncacheable_resource_count &&
self.surface_resource_count > 0 &&
self.gpu_backed_resource_count == 0 &&
self.byte_size > 0L
if ready {
"SurfaceTargetDescriptor finalization preflight ready"
} else {
"SurfaceTargetDescriptor finalization preflight pending"
}
}
///|
fn skia_gpu_context_status_label(
status : @skia_native.SkiaGpuContextSupportStatus,
) -> String {
match status {
@skia_native.SkiaGpuContextAvailable => "available"
@skia_native.SkiaGpuContextSkiaUnavailable => "skia-unavailable"
@skia_native.SkiaGpuContextAnonymous => "anonymous"
@skia_native.SkiaGpuContextMockUnsupported => "mock-unsupported"
@skia_native.SkiaGpuContextBackendUnsupported => "backend-unsupported"
@skia_native.SkiaGpuContextMetalOptInDisabled => "metal-opt-in-disabled"
@skia_native.SkiaGpuContextMetalHeadersUnavailable =>
"metal-headers-unavailable"
@skia_native.SkiaGpuContextMetalRuntimeUnavailable =>
"metal-runtime-unavailable"
@skia_native.SkiaGpuContextDirect3DOptInDisabled =>
"direct3d-opt-in-disabled"
@skia_native.SkiaGpuContextDirect3DHeadersUnavailable =>
"direct3d-headers-unavailable"
@skia_native.SkiaGpuContextDirect3DRuntimeUnavailable =>
"direct3d-runtime-unavailable"
@skia_native.SkiaGpuContextVulkanOptInDisabled => "vulkan-opt-in-disabled"
@skia_native.SkiaGpuContextVulkanHeadersUnavailable =>
"vulkan-headers-unavailable"
@skia_native.SkiaGpuContextVulkanRuntimeUnavailable =>
"vulkan-runtime-unavailable"
@skia_native.SkiaGpuContextEglOptInDisabled => "egl-opt-in-disabled"
@skia_native.SkiaGpuContextEglHeadersUnavailable =>
"egl-headers-unavailable"
@skia_native.SkiaGpuContextEglRuntimeUnavailable =>
"egl-runtime-unavailable"
}
}
///|
pub fn skia_gpu_metal_preflight_summary() -> String {
let context = @moui_skia.GpuContextDescriptor::metal(
b"moui-render-skia-metal-preflight",
)
let target = @moui_skia.SurfaceTargetDescriptor::gpu_n32_premul(
@moui_skia.ISize::new(1, 1),
gpu_context_key=Some(context.resource_key()),
)
let plan = target.resource_plan()
let target_plan_ready = target.has_gpu_context() &&
plan.count_kind(@moui_skia.GpuSurfaceResource) == 1 &&
plan.count_kind(@moui_skia.GpuContextResource) == 1 &&
plan.gpu_backed_count == 2
let status = @skia_native.Surface::gpu_context_support_status(context)
let target_label = if target_plan_ready {
"target-plan-ready"
} else {
"target-plan-pending"
}
let surface_label = if status.is_available() {
"explicit-offscreen-surface-api-ready"
} else {
"explicit-offscreen-surface-api-pending"
}
"Metal GPU opt-in preflight: context=\{skia_gpu_context_status_label(status)}; \{target_label}; \{surface_label}; window-present=pending"
}
///|
pub fn SkiaPresentTarget::new(
description~ : String,
present~ : (SkiaPixelFrame) -> Bool,
) -> SkiaPresentTarget {
{ description, present }
}
///|
pub fn SkiaPixelFrame::width(self : SkiaPixelFrame) -> Int {
self.width
}
///|
pub fn SkiaPixelFrame::height(self : SkiaPixelFrame) -> Int {
self.height
}
///|
pub fn SkiaPixelFrame::row_bytes(self : SkiaPixelFrame) -> Int {
self.row_bytes
}
///|
pub fn SkiaPixelFrame::scale_factor(self : SkiaPixelFrame) -> Double {
self.scale_factor
}
///|
pub fn SkiaPixelFrame::pixels(self : SkiaPixelFrame) -> Bytes {
self.pixels
}
///|
/// Adapt a live Skia renderer to the renderer-neutral host lifecycle.
pub fn SkiaRasterRenderer::to_renderer_session(
self : SkiaRasterRenderer,
image_source? : @render.HostImageSource = @render.HostImageSource::unavailable(),
image_decoder? : @render.RendererImageDecoder? = None,
native_surface? : @render.NativeSurface? = None,
) -> @render.RendererSession {
@render.RendererSession::new(
resize=metrics => {
match native_surface {
Some(surface) => surface.resize(metrics)
None => ()
}
self.resize(metrics) catch {
err => println("error resizing Skia renderer: \{err.message()}")
}
},
render_frame=submission => self.render_frame(submission.frame()),
text_system=() => self.text_system(),
present_count=() => self.present_count(),
recover=_reason => @render.RecoveryResult::FallbackToCpu,
dispose=() => self.dispose(),
image_records=() => self.image_resources(),
apply_image_load_completion=completion => {
self.apply_image_resource_load_completion(completion)
},
image_source~,
image_decoder~,
platform_view=@render.RendererPlatformViewCapability::new(
draw_platform_view_pixels=fn(
pixels,
src_width,
src_height,
src_stride,
dst_x,
dst_y,
dst_width,
dst_height,
) {
self.draw_platform_view_pixels(
pixels, src_width, src_height, src_stride, dst_x, dst_y, dst_width, dst_height,
)
},
set_platform_view_draw_fn=draw_fn => {
self.set_platform_view_draw_fn(draw_fn)
},
),
native_surface~,
)
}
///|
fn create_gpu_context(
surface_route : SkiaSurfaceRoute,
) -> @skia_native.GpuContext? raise SkiaRendererError {
match surface_route {
SkiaSurfaceRoute::RasterSurfaceRoute => None
SkiaSurfaceRoute::MetalGpuSurfaceRoute => {
let descriptor = @moui_skia.GpuContextDescriptor::metal(
b"moui-render-skia-metal-gpu",
)
match @skia_native.GpuContext::metal(descriptor) {
Some(context) => Some(context)
None => {
let status = @skia_native.Surface::gpu_context_support_status(
descriptor,
)
raise SkiaRendererError::GpuContextUnavailable(
skia_gpu_context_status_label(status),
)
}
}
}
SkiaSurfaceRoute::Direct3DGpuSurfaceRoute => {
let descriptor = @moui_skia.GpuContextDescriptor::direct3d12(
b"moui-render-skia-direct3d-gpu",
)
match @skia_native.GpuContext::direct3d12(descriptor) {
Some(context) => Some(context)
None => {
let status = @skia_native.Surface::gpu_context_support_status(
descriptor,
)
raise SkiaRendererError::GpuContextUnavailable(
skia_gpu_context_status_label(status),
)
}
}
}
SkiaSurfaceRoute::VulkanGpuSurfaceRoute => {
let descriptor = @moui_skia.GpuContextDescriptor::vulkan(
b"moui-render-skia-vulkan-gpu",
)
match @skia_native.GpuContext::vulkan(descriptor) {
Some(context) => Some(context)
None => {
let status = @skia_native.Surface::gpu_context_support_status(
descriptor,
)
raise SkiaRendererError::GpuContextUnavailable(
skia_gpu_context_status_label(status),
)
}
}
}
SkiaSurfaceRoute::EglGpuSurfaceRoute => {
let descriptor = @moui_skia.GpuContextDescriptor::opengl(
b"moui-render-skia-egl-gpu",
)
match @skia_native.GpuContext::egl(descriptor) {
Some(context) => Some(context)
None => {
let status = @skia_native.Surface::gpu_context_support_status(
descriptor,
)
raise SkiaRendererError::GpuContextUnavailable(
skia_gpu_context_status_label(status),
)
}
}
}
}
}
///|
fn create_surface(
metrics : @core.SurfaceMetrics,
surface_route : SkiaSurfaceRoute,
gpu_context : @skia_native.GpuContext?,
) -> @skia_native.Surface raise SkiaRendererError {
let target = skia_surface_target(metrics, surface_route, gpu_context)
match surface_route {
SkiaSurfaceRoute::RasterSurfaceRoute =>
match @skia_native.Surface::for_target(target) {
Some(surface) => surface
None => raise SkiaRendererError::SurfaceUnavailable
}
SkiaSurfaceRoute::MetalGpuSurfaceRoute =>
match gpu_context {
None =>
raise SkiaRendererError::GpuContextUnavailable(
"missing-metal-gpu-context",
)
Some(gpu_context) =>
match
@skia_native.Surface::for_target_with_gpu_context(
target, gpu_context,
) {
Some(surface) => surface
None => raise SkiaRendererError::SurfaceUnavailable
}
}
SkiaSurfaceRoute::Direct3DGpuSurfaceRoute =>
match gpu_context {
None =>
raise SkiaRendererError::GpuContextUnavailable(
"missing-direct3d-gpu-context",
)
Some(gpu_context) =>
match
@skia_native.Surface::for_target_with_gpu_context(
target, gpu_context,
) {
Some(surface) => surface
None => raise SkiaRendererError::SurfaceUnavailable
}
}
SkiaSurfaceRoute::VulkanGpuSurfaceRoute =>
match gpu_context {
None =>
raise SkiaRendererError::GpuContextUnavailable(
"missing-vulkan-gpu-context",
)
Some(gpu_context) =>
match
@skia_native.Surface::for_target_with_gpu_context(
target, gpu_context,
) {
Some(surface) => surface
None => raise SkiaRendererError::SurfaceUnavailable
}
}
SkiaSurfaceRoute::EglGpuSurfaceRoute =>
match gpu_context {
None =>
raise SkiaRendererError::GpuContextUnavailable(
"missing-egl-gpu-context",
)
Some(gpu_context) =>
match
@skia_native.Surface::for_target_with_gpu_context(
target, gpu_context,
) {
Some(surface) => surface
None => raise SkiaRendererError::SurfaceUnavailable
}
}
}
}
///|
fn skia_surface_target(
metrics : @core.SurfaceMetrics,
surface_route : SkiaSurfaceRoute,
gpu_context : @skia_native.GpuContext?,
) -> @moui_skia.SurfaceTargetDescriptor {
let size = @moui_skia.ISize::new(
skia_dim_to_int(metrics.physical_size.width),
skia_dim_to_int(metrics.physical_size.height),
)
skia_surface_target_for_physical_size(size, surface_route, gpu_context)
}
///|
fn skia_surface_target_for_physical_size(
size : @moui_skia.ISize,
surface_route : SkiaSurfaceRoute,
gpu_context : @skia_native.GpuContext?,
) -> @moui_skia.SurfaceTargetDescriptor {
match surface_route {
SkiaSurfaceRoute::RasterSurfaceRoute =>
@moui_skia.SurfaceTargetDescriptor::raster_n32_premul(size)
SkiaSurfaceRoute::MetalGpuSurfaceRoute =>
@moui_skia.SurfaceTargetDescriptor::gpu_n32_premul(
size,
gpu_context_key=match gpu_context {
Some(gpu_context) => Some(gpu_context.resource_key())
None => None
},
)
SkiaSurfaceRoute::Direct3DGpuSurfaceRoute =>
@moui_skia.SurfaceTargetDescriptor::gpu_n32_premul(
size,
gpu_context_key=match gpu_context {
Some(gpu_context) => Some(gpu_context.resource_key())
None => None
},
)
SkiaSurfaceRoute::VulkanGpuSurfaceRoute =>
@moui_skia.SurfaceTargetDescriptor::gpu_n32_premul(
size,
gpu_context_key=match gpu_context {
Some(gpu_context) => Some(gpu_context.resource_key())
None => None
},
)
SkiaSurfaceRoute::EglGpuSurfaceRoute =>
@moui_skia.SurfaceTargetDescriptor::gpu_n32_premul(
size,
gpu_context_key=match gpu_context {
Some(gpu_context) => Some(gpu_context.resource_key())
None => None
},
)
}
}
///|
fn skia_raster_surface_target(
metrics : @core.SurfaceMetrics,
) -> @moui_skia.SurfaceTargetDescriptor {
@moui_skia.SurfaceTargetDescriptor::raster_n32_premul(
@moui_skia.ISize::new(
skia_dim_to_int(metrics.physical_size.width),
skia_dim_to_int(metrics.physical_size.height),
),
)
}
///|
fn raster_surface_preflight(
metrics : @core.SurfaceMetrics,
) -> SkiaRasterSurfacePreflight {
let target = skia_raster_surface_target(metrics)
let command_list = @moui_skia.RenderCommandList::for_target(target).clear(
@moui_skia.Color::transparent(),
)
let frame = command_list.frame_descriptor(target)
let finalization = frame.finalization_descriptor()
let submission = frame.submission_descriptor()
let resource_plan = finalization.resource_plan
SkiaRasterSurfacePreflight::{
physical_width: target.dimensions().width,
physical_height: target.dimensions().height,
target_ready: !target.resource_key().is_empty(),
frame_ready: frame.is_ready(),
finalization_ready: finalization.is_ready(),
submission_ready: submission.is_ready(),
resource_count: resource_plan.length(),
cacheable_resource_count: resource_plan.cacheable_count,
uncacheable_resource_count: resource_plan.uncacheable_count,
gpu_backed_resource_count: resource_plan.gpu_backed_count,
surface_resource_count: resource_plan.count_kind(@moui_skia.SurfaceResource),
byte_size: resource_plan.byte_size,
}
}
///|
pub fn SkiaRasterRenderer::resize(
self : SkiaRasterRenderer,
metrics : @core.SurfaceMetrics,
) -> Unit raise SkiaRendererError {
self.metrics = metrics
self.surface = match self.gpu_target {
Some(gpu_target) =>
if self.gpu_surface_attached {
match (gpu_target.create_window_surface)(self.gpu_context, metrics) {
Some(surface) => surface
None => {
self.gpu_surface_attached = false
raise SkiaRendererError::SurfaceUnavailable
}
}
} else {
create_surface(metrics, self.surface_route, self.gpu_context)
}
None => create_surface(metrics, self.surface_route, self.gpu_context)
}
self.clear_layer_cache()
}
///|
fn SkiaRasterRenderer::ensure_gpu_window_surface(
self : SkiaRasterRenderer,
) -> Unit raise SkiaRendererError {
if self.gpu_surface_attached {
return
}
match self.gpu_target {
Some(gpu_target) =>
match (gpu_target.create_window_surface)(self.gpu_context, self.metrics) {
Some(surface) => {
self.surface = surface
self.gpu_surface_attached = true
}
None => raise SkiaRendererError::SurfaceUnavailable
}
None => self.gpu_surface_attached = true
}
}
///|
pub fn SkiaRasterRenderer::surface_route(
self : SkiaRasterRenderer,
) -> SkiaSurfaceRoute {
self.surface_route
}
///|
pub fn SkiaRasterRenderer::surface_is_gpu(self : SkiaRasterRenderer) -> Bool {
self.surface.descriptor().is_gpu()
}
///|
/// Returns which present path the renderer is bound to. `CpuPixelFrame` reads
/// pixels back via `Surface::read_pixels` and hands a `SkiaPixelFrame` to the
/// platform presenter; `HostGpuSurface` lets the platform present a
/// window-backed GPU drawable directly with no CPU readback.
pub fn SkiaRasterRenderer::target_kind(
self : SkiaRasterRenderer,
) -> SkiaPresentKind {
match self.gpu_target {
Some(_) => SkiaPresentKind::HostGpuSurface
None => SkiaPresentKind::CpuPixelFrame
}
}
///|
/// Returns the bound `HostGpuPresentTarget` when the renderer is on the direct
/// GPU present path. Returns `None` for the legacy CPU pixel-frame route.
pub fn SkiaRasterRenderer::gpu_target(
self : SkiaRasterRenderer,
) -> HostGpuPresentTarget? {
self.gpu_target
}
///|
pub fn SkiaRasterRenderer::surface_diagnostic_summary(
self : SkiaRasterRenderer,
) -> String {
let context = match self.gpu_context {
Some(gpu_context) =>
skia_gpu_context_status_label(gpu_context.support_status())
None => "none"
}
let gpu_surface = if self.surface_is_gpu() { "true" } else { "false" }
let present_kind = match self.target_kind() {
SkiaPresentKind::CpuPixelFrame => "cpu-pixel-frame"
SkiaPresentKind::HostGpuSurface => "host-gpu-surface"
}
"surface_route=\{self.surface_route.label()}; surface_gpu=\{gpu_surface}; gpu_context=\{context}; present_kind=\{present_kind}; dimensions=\{self.surface.width()}x\{self.surface.height()}"
}
///|
fn SkiaRasterRenderer::read_frame(
self : SkiaRasterRenderer,
) -> SkiaPixelFrame raise SkiaRendererError {
match self.surface.read_pixels() {
Some(pixmap) =>
{
width: pixmap.width(),
height: pixmap.height(),
row_bytes: pixmap.row_bytes(),
scale_factor: self.metrics.scale_factor,
pixels: pixmap.pixels,
}
None => raise SkiaRendererError::ReadPixelsUnavailable
}
}