///|
/// Build a Skia CPU raster provider from the renderer-neutral host contract.
pub fn raster(
font_resolution? : SkiaFontResolution = SkiaFontResolution::SystemFontMgr,
) -> @render.RendererProvider {
let image_decoder = skia_image_decoder()
let caps = skia_provider_capabilities(
provider_id="skia-raster",
backend=@render.RendererBackendKind::SkiaRasterNative,
)
@render.RendererProvider::new(
id="skia-raster",
descriptor=skia_raster_native_descriptor(),
capabilities=() => caps,
bind=surface => {
match @render_common.host_surface_metrics_error(surface) {
Some(message) => return @render.RendererBindResult::Rejected(message)
None => ()
}
guard surface.cpu_present() is Some(host) else {
return @render.RendererBindResult::Rejected(
"skia-raster requires a CPU presenter",
)
}
let target = skia_present_target_from_host(Some(host))
let renderer = create_with_present_target(
surface.metrics(),
target,
font_resolution~,
async_image_loading=true,
) catch {
err =>
return @render.RendererBindResult::Rejected(
"skia-raster unavailable: " + err.message(),
)
}
@render.RendererBindResult::Bound(
renderer.to_renderer_session(
image_source=surface
.image_source()
.unwrap_or(@render.HostImageSource::unavailable()),
image_decoder=Some(image_decoder),
),
)
},
)
}
///|
/// Build a Skia direct GPU provider. The platform policy belongs to this
/// renderer module and interprets the opaque native handles after binding.
pub fn gpu(
platform~ : NativeGpuPlatform,
font_resolution? : SkiaFontResolution = SkiaFontResolution::SystemFontMgr,
) -> @render.RendererProvider {
let image_decoder = skia_image_decoder()
let caps = skia_provider_capabilities(
provider_id="skia-hybrid",
backend=@render.RendererBackendKind::SkiaGpuNative,
)
@render.RendererProvider::new(
id="skia-hybrid",
descriptor=skia_gpu_native_descriptor(),
capabilities=() => caps,
bind=surface => {
match @render_common.host_surface_metrics_error(surface) {
Some(message) => return @render.RendererBindResult::Rejected(message)
None => ()
}
guard surface.native_surface() is Some(native_surface) else {
return @render.RendererBindResult::Rejected(
"skia-hybrid requires a native surface capability",
)
}
match skia_native_surface_error(platform, native_surface) {
Some(message) => return @render.RendererBindResult::Rejected(message)
None => ()
}
if !skia_gpu_runtime_available(platform) {
return @render.RendererBindResult::Rejected(
"skia-hybrid GPU runtime unavailable for \{NativePlatformSurface::platform_label(platform)}",
)
}
let session = create_skia_gpu_host_renderer(
surface.metrics(),
skia_present_target_from_host(surface.cpu_present()),
platform,
native_surface,
font_resolution,
image_source=surface
.image_source()
.unwrap_or(@render.HostImageSource::unavailable()),
image_decoder=Some(image_decoder),
)
@render.RendererBindResult::Bound(session)
},
)
}
///|
fn skia_image_decoder() -> @render.RendererImageDecoder {
@render.RendererImageDecoder::new(decode=(source, bytes) => {
match bytes {
Some(data) =>
skia_image_load_completion_from_bytes(source, data, background_io=true)
None => skia_image_load_completion(source)
}
})
}
///|
fn skia_present_target_from_host(
host : @render.PresentTarget?,
) -> SkiaPresentTarget {
match host {
Some(host) =>
SkiaPresentTarget::new(description=host.description(), present=frame => {
host.present(
@render.PixelFrame::new(
width=frame.width(),
height=frame.height(),
row_bytes=frame.row_bytes(),
scale_factor=frame.scale_factor(),
pixels=frame.pixels(),
),
)
})
None =>
SkiaPresentTarget::new(description="missing host CPU presenter", present=_ => {
false
})
}
}
///|
fn create_skia_gpu_host_renderer(
metrics : @core.SurfaceMetrics,
target : SkiaPresentTarget,
platform : NativeGpuPlatform,
native_surface : @render.NativeSurface,
font_resolution : SkiaFontResolution,
image_source? : @render.HostImageSource = @render.HostImageSource::unavailable(),
image_decoder? : @render.RendererImageDecoder? = None,
) -> @render.RendererSession {
let route = NativePlatformSurface::surface_route(platform)
let gpu_target = HostGpuPresentTarget::new(
description="Skia direct host GPU presenter",
create_window_surface=(context, next_metrics) => {
native_surface.resize(next_metrics)
match context {
Some(context) =>
skia_gpu_window_surface(
context, platform, native_surface, next_metrics,
)
None => None
}
},
present_gpu=(context, surface) => {
let next = match context {
Some(context) =>
skia_gpu_present_and_acquire_next(
surface, context, platform, native_surface,
)
None => None
}
match next {
Some(next_surface) => {
ignore(native_surface.present(1UL))
HostGpuPresentOutcome::new(
succeeded=true,
next_surface=Some(next_surface),
)
}
None => HostGpuPresentOutcome::new(succeeded=false)
}
},
)
let gpu_renderer = create_with_present_target_and_route(
metrics,
target,
surface_route=route,
gpu_target=Some(gpu_target),
font_resolution~,
async_image_loading=true,
) catch {
err => {
// GPU context/surface creation can still fail after a positive native
// capability probe. Keep the selected factory usable by constructing the
// neutral CPU presenter immediately; the same path also handles hosts
// that expose a stale drawable during attach.
println(
"Skia GPU renderer unavailable at attach; falling back to raster: " +
err.message(),
)
return create_skia_raster_host_renderer(
metrics,
target,
font_resolution,
image_source~,
image_decoder~,
native_surface=Some(native_surface),
)
}
}
let fallback : Ref[SkiaRasterRenderer?] = Ref(None)
let fallback_active : Ref[Bool] = Ref(false)
let activate_fallback : (@core.SurfaceMetrics) -> Bool = next_metrics => {
if fallback_active.val {
return true
}
// Releasing the GPU surface before the CPU presenter locks the native
// window is required on Android and HarmonyOS (both reuse the same host
// window for the raster fallback).
gpu_renderer.dispose()
let raster : SkiaRasterRenderer? = Some(
create_with_present_target(
next_metrics,
target,
font_resolution~,
async_image_loading=true,
),
) catch {
err => {
println(
"Skia raster fallback unavailable after GPU failure: " + err.message(),
)
None
}
}
match raster {
Some(renderer) => {
fallback.val = Some(renderer)
fallback_active.val = true
true
}
None => false
}
}
let with_active : ((SkiaRasterRenderer) -> Unit) -> Unit = action => {
match fallback.val {
Some(renderer) if fallback_active.val => action(renderer)
_ => action(gpu_renderer)
}
}
let active_render_frame : (@render.RenderFrameSubmission) -> @render.RenderFrameResult = submission => {
let frame = submission.frame()
match fallback.val {
Some(renderer) if fallback_active.val => renderer.render_frame(frame)
_ => {
let result = gpu_renderer.render_frame(frame)
if result.presented {
result
} else if activate_fallback(gpu_renderer.metrics()) {
match fallback.val {
Some(renderer) => renderer.render_frame(frame)
None => result
}
} else {
result
}
}
}
}
@render.RendererSession::new(
resize=next_metrics => {
if fallback_active.val {
match fallback.val {
Some(renderer) =>
renderer.resize(next_metrics) catch {
err =>
println("error resizing Skia raster fallback: " + err.message())
}
None => ()
}
} else {
gpu_renderer.resize(next_metrics) catch {
err => {
println("error resizing Skia GPU renderer: " + err.message())
ignore(activate_fallback(next_metrics))
}
}
}
},
render_frame=active_render_frame,
text_system=() => {
match fallback.val {
Some(renderer) if fallback_active.val => renderer.text_system()
_ => gpu_renderer.text_system()
}
},
present_count=() => {
match fallback.val {
Some(renderer) if fallback_active.val => renderer.present_count()
_ => gpu_renderer.present_count()
}
},
dispose=() => {
gpu_renderer.dispose()
match fallback.val {
Some(renderer) => renderer.dispose()
None => ()
}
},
image_records=() => {
match fallback.val {
Some(renderer) if fallback_active.val => renderer.image_resources()
_ => gpu_renderer.image_resources()
}
},
apply_image_load_completion=completion => {
match fallback.val {
Some(renderer) if fallback_active.val =>
renderer.apply_image_resource_load_completion(completion)
_ => gpu_renderer.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,
) {
with_active(action => {
action.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 => {
with_active(action => action.set_platform_view_draw_fn(draw_fn))
},
),
gpu_recovery=@render.RendererGpuRecoveryCapability::new(gpu_fallback_active=() => {
fallback_active.val
}),
native_surface=Some(native_surface),
)
}
///|
/// Construct the synchronous CPU host path used both by the explicit raster
/// factory and by GPU creation/recovery failures.
fn create_skia_raster_host_renderer(
metrics : @core.SurfaceMetrics,
target : SkiaPresentTarget,
font_resolution : SkiaFontResolution,
image_source? : @render.HostImageSource = @render.HostImageSource::unavailable(),
image_decoder? : @render.RendererImageDecoder? = None,
native_surface? : @render.NativeSurface? = None,
) -> @render.RendererSession {
create_with_present_target(
metrics,
target,
font_resolution~,
async_image_loading=true,
).to_renderer_session(image_source~, image_decoder~, native_surface~) catch {
err => abort(err.message())
}
}
///|
fn skia_gpu_surface_descriptor(
metrics : @core.SurfaceMetrics,
) -> @moui_skia.SurfaceDescriptor {
@moui_skia.SurfaceDescriptor::gpu_n32_premul(
@moui_skia.ISize::new(
metrics.physical_size.width.ceil().max(1.0).to_int(),
metrics.physical_size.height.ceil().max(1.0).to_int(),
),
)
}
///|
fn skia_gpu_window_surface(
context : @skia_native.GpuContext,
platform : NativeGpuPlatform,
native_surface : @render.NativeSurface,
metrics : @core.SurfaceMetrics,
) -> @skia_native.Surface? {
let surface = skia_gpu_surface_descriptor(metrics)
match platform {
NativeGpuPlatform::MacOS | NativeGpuPlatform::IOS =>
@skia_native.Surface::metal_window_surface(
context,
native_surface.surface_handle(),
surface,
)
NativeGpuPlatform::Windows =>
@skia_native.Surface::direct3d_window_surface(
context,
native_surface.surface_handle(),
surface,
)
NativeGpuPlatform::Android =>
@skia_native.Surface::vulkan_window_surface(
context,
native_surface.surface_handle(),
surface,
)
NativeGpuPlatform::Linux =>
@skia_native.Surface::vulkan_wayland_surface(
context,
native_surface.display_handle().unwrap(),
native_surface.surface_handle(),
surface,
)
NativeGpuPlatform::HarmonyOS =>
@skia_native.Surface::egl_window_surface(
context,
native_surface.surface_handle(),
surface,
)
}
}
///|
fn skia_gpu_present_and_acquire_next(
surface : @skia_native.Surface,
context : @skia_native.GpuContext,
platform : NativeGpuPlatform,
native_surface : @render.NativeSurface,
) -> @skia_native.Surface? {
match platform {
NativeGpuPlatform::MacOS | NativeGpuPlatform::IOS =>
@skia_native.Surface::metal_present_and_acquire_next(
surface,
context,
native_surface.surface_handle(),
)
NativeGpuPlatform::Windows =>
@skia_native.Surface::direct3d_present_and_acquire_next(
surface,
context,
native_surface.surface_handle(),
)
NativeGpuPlatform::Linux | NativeGpuPlatform::Android =>
@skia_native.Surface::vulkan_present_and_acquire_next(
surface,
context,
native_surface.surface_handle(),
)
NativeGpuPlatform::HarmonyOS =>
@skia_native.Surface::egl_present_and_acquire_next(
surface,
context,
native_surface.surface_handle(),
)
}
}
///|
fn skia_gpu_runtime_available(platform : NativeGpuPlatform) -> Bool {
match platform {
NativeGpuPlatform::MacOS | NativeGpuPlatform::IOS =>
@skia_native.Surface::metal_gpu_context_runtime_available()
NativeGpuPlatform::Windows =>
@skia_native.Surface::direct3d_gpu_context_runtime_available()
NativeGpuPlatform::Linux | NativeGpuPlatform::Android =>
@skia_native.Surface::vulkan_gpu_context_runtime_available()
NativeGpuPlatform::HarmonyOS =>
@skia_native.Surface::egl_gpu_context_runtime_available()
}
}
///|
fn skia_native_surface_error(
platform : NativeGpuPlatform,
surface : @render.NativeSurface,
) -> String? {
if surface.surface_handle() == 0UL {
return Some("skia-hybrid requires a non-zero surface handle")
}
match platform {
NativeGpuPlatform::Linux =>
match surface.display_handle() {
Some(display) if display != 0UL => None
_ => Some("skia-hybrid linux requires a non-zero display handle")
}
_ => None
}
}
///|
/// Resolve the environment policy entirely inside the Skia renderer package.
pub fn from_env(
platform~ : NativeGpuPlatform,
) -> Array[@render.RendererProvider] {
let mode = match @env.get_env_var("MOUI_SKIA_RENDERER") {
Some(value) => NativeRendererMode::parse(value)
None => None
}
providers_for_mode(platform, mode)
}
///|
fn providers_for_mode(
platform : NativeGpuPlatform,
mode : NativeRendererMode?,
) -> Array[@render.RendererProvider] {
match mode {
Some(NativeRendererMode::SkiaRaster) => [raster()]
Some(NativeRendererMode::SkiaGpu) => [gpu(platform~)]
Some(NativeRendererMode::Auto) | None => [gpu(platform~), raster()]
}
}