///|
/// MoUI renderer facade. Core value types (`RendererBackendKind`,
/// `@core.SurfaceMetrics`, `RenderFrameResult`, …) are defined in this
/// file. The package also owns opaque host-surface capabilities, image/frame
/// DTOs, capability reporting, and the provider/session plugin system (ADR
/// 0027) in `provider_contract.mbt` and `renderer_session.mbt`.
///
/// ## RendererProvider plugin system
///
/// See `provider_contract.mbt` for:
/// - `RendererProvider` — static registration and delayed surface binding
/// - `RendererSession` — complete per-window live renderer lifecycle
/// - `HostSurface` — opaque platform capability wrapper
///
/// Provider selection and registries live in `moui/render/common`. Concrete
/// providers live in sub-packages:
/// - `moui_skia_renderer`
/// - `moui_wgpu_renderer`
/// - `moui_sun_renderer`
/// - `moui_web_renderer` and `moui_web_renderer/canvas2d`
pub(all) enum RendererBackendKind {
NativeWgpu
WebGpuWasm
SkiaRasterNative
SkiaGpuNative
SunRasterNative
Canvas2DWasm
} derive(Eq, Debug, ToJson)
///|
pub(all) enum RendererCapabilityStatus {
Supported
Unavailable
} derive(Eq, Debug, ToJson)
///|
pub(all) enum RendererFamily {
Wgpu
WebGpu
Skia
Sun
Canvas2D
} derive(Eq, Debug, ToJson)
///|
pub(all) enum RendererPresentationModel {
HostGpuSurface
WebCanvas
CpuPixelFrame
ExternalSurface
} derive(Eq, Debug, ToJson)
///|
pub(all) enum RendererTarget {
Native
WasmGc
Web
} derive(Eq, Debug, ToJson)
///|
pub(all) enum RendererSelection {
Default
Backend(RendererBackendKind)
Family(RendererFamily)
} derive(Eq, Debug, ToJson)
///|
/// Renderer-level window identifier. Mirror of `WindowId` in the host
/// package — the host converts between the two at the API boundary so that
/// `moui/render` never imports `moui/backend`.
pub(all) struct RenderWindowId {
value : Int
} derive(Eq, Debug, ToJson)
///|
pub fn RenderWindowId::new(value : Int) -> RenderWindowId {
{ value, }
}
///|
pub(all) struct RendererBackendInfo {
backend : RendererBackendKind
status : RendererCapabilityStatus
can_render : Bool
can_resize : Bool
owns_surface : Bool
description : String
} derive(Eq, Debug, ToJson)
///|
pub(all) struct RendererDescriptor {
backend : RendererBackendKind
family : RendererFamily
presentation : RendererPresentationModel
supported_targets : Array[RendererTarget]
capabilities : Array[RendererBackendFeatureCapability]
description : String
} derive(Eq, Debug, ToJson)
///|
pub(all) struct RenderFrameInput {
metrics : @core.SurfaceMetrics
commands : Array[@core.DrawCommand]
damage : @core.DamageRegion
clear_color : @core.Color?
} derive(Eq, Debug, ToJson)
///|
pub(all) struct RenderFrameResult {
presented : Bool
queued : Bool
cache_hit_count : Int
cache_miss_count : Int
cache_update_count : Int
cache_evict_count : Int
} derive(Eq, Debug, ToJson)
///|
/// Completion emitted by an asynchronous renderer worker. A queued frame is
/// not considered presented until the host drains this record.
pub(all) enum RenderPresentStatus {
Recorded
Presented
Dropped
FallbackToRaster
} derive(Eq, Debug, ToJson)
///|
/// Renderer-worker completion metadata. The frame token is the only identity
/// used by backend frame owners to accept or reject an asynchronous result.
pub(all) struct RenderPresentCompletion {
token : FrameToken
submitted_at_ms : Double
completed_at_ms : Double
presented_at_ms : Double
status : RenderPresentStatus
gpu_context_generation : Int
} derive(Eq, Debug, ToJson)
///|
pub(all) enum WgpuSurfaceSourceKind {
NativeHostSurface
WebCanvasSurface
} derive(Eq, Debug, ToJson)
///|
pub(all) struct WgpuSurfaceContract {
source : WgpuSurfaceSourceKind
backend : RendererBackendKind
ready : Bool
description : String
} derive(Eq, Debug, ToJson)
///|
pub fn RendererBackendInfo::new(
backend~ : RendererBackendKind,
status~ : RendererCapabilityStatus,
can_render~ : Bool,
can_resize~ : Bool,
owns_surface~ : Bool,
description? : String = "",
) -> RendererBackendInfo {
{ backend, status, can_render, can_resize, owns_surface, description }
}
///|
pub fn RendererBackendKind::label(self : RendererBackendKind) -> String {
match self {
NativeWgpu => "native-wgpu"
WebGpuWasm => "webgpu-wasm"
SkiaRasterNative => "skia-raster-native"
SkiaGpuNative => "skia-gpu-native"
SunRasterNative => "sun-raster-native"
Canvas2DWasm => "canvas2d-wasm"
}
}
///|
pub fn RendererBackendKind::short_label(self : RendererBackendKind) -> String {
match self {
NativeWgpu => "wgpu"
WebGpuWasm => "webgpu"
SkiaRasterNative => "skia"
SkiaGpuNative => "skia-gpu"
SunRasterNative => "sun"
Canvas2DWasm => "canvas2d"
}
}
///|
pub fn RendererFamily::label(self : RendererFamily) -> String {
match self {
Wgpu => "wgpu"
WebGpu => "webgpu"
Skia => "skia"
Sun => "sun"
Canvas2D => "canvas2d"
}
}
///|
pub fn RendererPresentationModel::label(
self : RendererPresentationModel,
) -> String {
match self {
HostGpuSurface => "host-gpu-surface"
WebCanvas => "web-canvas"
CpuPixelFrame => "cpu-pixel-frame"
ExternalSurface => "external-surface"
}
}
///|
pub fn RendererSelection::native_default() -> RendererSelection {
RendererSelection::Backend(RendererBackendKind::SkiaRasterNative)
}
///|
pub fn RendererSelection::web_default() -> RendererSelection {
RendererSelection::Backend(RendererBackendKind::WebGpuWasm)
}
///|
pub fn RendererSelection::matches_backend(
self : RendererSelection,
backend : RendererBackendKind,
) -> Bool {
match self {
Default => false
Backend(selected) => selected == backend
Family(family) => backend.family() == family
}
}
///|
pub fn RendererBackendKind::family(
self : RendererBackendKind,
) -> RendererFamily {
match self {
NativeWgpu => RendererFamily::Wgpu
WebGpuWasm => RendererFamily::WebGpu
SkiaRasterNative | SkiaGpuNative => RendererFamily::Skia
SunRasterNative => RendererFamily::Sun
Canvas2DWasm => RendererFamily::Canvas2D
}
}
///|
pub fn RendererDescriptor::new(
backend~ : RendererBackendKind,
family~ : RendererFamily,
presentation~ : RendererPresentationModel,
supported_targets~ : Array[RendererTarget],
capabilities~ : Array[RendererBackendFeatureCapability],
description? : String = "",
) -> RendererDescriptor {
{
backend,
family,
presentation,
supported_targets,
capabilities,
description,
}
}
///|
pub fn RenderFrameInput::new(
metrics~ : @core.SurfaceMetrics,
commands~ : Array[@core.DrawCommand],
damage? : @core.DamageRegion = @core.DamageRegion::full(
"legacy render frame input",
),
clear_color? : @core.Color? = None,
) -> RenderFrameInput {
{ metrics, commands, damage, clear_color }
}
///|
pub fn RenderFrameResult::new(
presented? : Bool = true,
queued? : Bool = false,
cache_hit_count? : Int = 0,
cache_miss_count? : Int = 0,
cache_update_count? : Int = 0,
cache_evict_count? : Int = 0,
) -> RenderFrameResult {
{
presented,
queued,
cache_hit_count,
cache_miss_count,
cache_update_count,
cache_evict_count,
}
}
///|
pub fn RenderPresentCompletion::new(
token~ : FrameToken,
submitted_at_ms~ : Double,
completed_at_ms~ : Double,
presented_at_ms~ : Double,
status~ : RenderPresentStatus,
gpu_context_generation~ : Int,
) -> RenderPresentCompletion {
{
token,
submitted_at_ms,
completed_at_ms,
presented_at_ms,
status,
gpu_context_generation,
}
}
///|
pub fn RenderPresentCompletion::token(
self : RenderPresentCompletion,
) -> FrameToken {
self.token
}
///|
pub fn RenderPresentCompletion::submitted_at_ms(
self : RenderPresentCompletion,
) -> Double {
self.submitted_at_ms
}
///|
pub fn RenderPresentCompletion::completed_at_ms(
self : RenderPresentCompletion,
) -> Double {
self.completed_at_ms
}
///|
pub fn RenderPresentCompletion::presented_at_ms(
self : RenderPresentCompletion,
) -> Double {
self.presented_at_ms
}
///|
pub fn RenderPresentCompletion::status(
self : RenderPresentCompletion,
) -> RenderPresentStatus {
self.status
}
///|
pub fn RenderPresentCompletion::gpu_context_generation(
self : RenderPresentCompletion,
) -> Int {
self.gpu_context_generation
}
///|
pub fn WgpuSurfaceContract::new(
source~ : WgpuSurfaceSourceKind,
backend~ : RendererBackendKind,
ready~ : Bool,
description? : String = "",
) -> WgpuSurfaceContract {
{ source, backend, ready, description }
}
///|
pub struct RectBatch {
priv rect : @core.Rect
priv color : @core.Color
} derive(Eq, Debug, ToJson)
///|
pub struct RectVertex {
priv x : Double
priv y : Double
priv r : Double
priv g : Double
priv b : Double
priv a : Double
} derive(Eq, Debug, ToJson)
///|
pub fn RectBatch::new(rect~ : @core.Rect, color~ : @core.Color) -> RectBatch {
{ rect, color }
}
///|
pub fn RectBatch::rect(self : RectBatch) -> @core.Rect {
self.rect
}
///|
pub fn RectBatch::color(self : RectBatch) -> @core.Color {
self.color
}
///|
pub fn RectVertex::x(self : RectVertex) -> Double {
self.x
}
///|
pub fn RectVertex::y(self : RectVertex) -> Double {
self.y
}
///|
pub fn RectVertex::r(self : RectVertex) -> Double {
self.r
}
///|
pub fn RectVertex::g(self : RectVertex) -> Double {
self.g
}
///|
pub fn RectVertex::b(self : RectVertex) -> Double {
self.b
}
///|
pub fn RectVertex::a(self : RectVertex) -> Double {
self.a
}
///|
pub fn collect_rect_batches(
commands : Array[@core.DrawCommand],
) -> Array[RectBatch] {
let batches : Array[RectBatch] = []
for command in commands {
match command {
@core.DrawCommand::FillRect(rect, color) => batches.push({ rect, color })
@core.DrawCommand::FillRoundedRect(rounded, color) =>
batches.push({ rect: rounded.rect, color })
@core.DrawCommand::StrokeRect(rect, color, width) =>
append_stroke_rect_batches(batches, rect, color, width)
@core.DrawCommand::StrokeRoundedRect(rounded, color, width) =>
append_stroke_rect_batches(batches, rounded.rect, color, width)
@core.DrawCommand::FillRoundedRectBrush(rounded, brush) =>
batches.push({ rect: rounded.rect, color: brush_fallback_color(brush) })
@core.DrawCommand::StrokeRoundedRectBrush(rounded, brush, width) =>
append_stroke_rect_batches(
batches,
rounded.rect,
brush_fallback_color(brush),
width,
)
@core.DrawCommand::DrawShadow(shadow) =>
append_shadow_batches(batches, shadow)
@core.DrawCommand::Clear(_)
| @core.DrawCommand::DrawText(_)
| @core.DrawCommand::DrawImage(_)
| @core.DrawCommand::PushClip(_)
| @core.DrawCommand::PopClip
| @core.DrawCommand::PushRoundedClip(_)
| @core.DrawCommand::PopRoundedClip
| @core.DrawCommand::PushTransform(_)
| @core.DrawCommand::PopTransform
| @core.DrawCommand::PushOpacity(_)
| @core.DrawCommand::PopOpacity
| @core.DrawCommand::PushLayer(_)
| @core.DrawCommand::PopLayer
| @core.DrawCommand::BeginRetainedLayer(_)
| @core.DrawCommand::EndRetainedLayer(_)
| @core.DrawCommand::PushFilter(_)
| @core.DrawCommand::PopFilter
| @core.DrawCommand::DrawPath(_)
| @core.DrawCommand::DrawShaderEffect(_) => ()
}
}
batches
}
///|
fn brush_fallback_color(brush : @core.Brush) -> @core.Color {
match brush {
@core.Brush::Solid(color) => color
@core.Brush::LinearGradient(gradient) => gradient.start_color
@core.Brush::RadialGradient(gradient) => gradient.center_color
}
}
///|
fn append_shadow_batches(
batches : Array[RectBatch],
shadow : @core.ShadowSpec,
) -> Unit {
let layers = 3
for index in 0.. Unit {
if width <= 0.0 || rect.size.width <= 0.0 || rect.size.height <= 0.0 {
return
}
let stroke_width = clamp_stroke_width(width, rect)
let x = rect.origin.x
let y = rect.origin.y
let w = rect.size.width
let h = rect.size.height
batches.push({
rect: @core.Rect::new(x~, y~, width=w, height=stroke_width),
color,
})
batches.push({
rect: @core.Rect::new(
x~,
y=y + h - stroke_width,
width=w,
height=stroke_width,
),
color,
})
if h > stroke_width * 2.0 {
batches.push({
rect: @core.Rect::new(
x~,
y=y + stroke_width,
width=stroke_width,
height=h - stroke_width * 2.0,
),
color,
})
batches.push({
rect: @core.Rect::new(
x=x + w - stroke_width,
y=y + stroke_width,
width=stroke_width,
height=h - stroke_width * 2.0,
),
color,
})
}
}
///|
fn clamp_stroke_width(width : Double, rect : @core.Rect) -> Double {
let max_width = rect.size.width.min(rect.size.height) / 2.0
if width > max_width {
max_width
} else {
width
}
}
///|
pub fn rect_batches_to_vertices(
batches : Array[RectBatch],
size : @core.Size,
) -> Array[RectVertex] {
let vertices : Array[RectVertex] = []
for batch in batches {
append_rect_vertices(vertices, batch, size)
}
vertices
}
///|
pub fn rect_vertices_to_bytes(vertices : Array[RectVertex]) -> Bytes {
let out : Array[Byte] = []
for vertex in vertices {
push_f32le(out, vertex.x)
push_f32le(out, vertex.y)
push_f32le(out, vertex.r)
push_f32le(out, vertex.g)
push_f32le(out, vertex.b)
push_f32le(out, vertex.a)
}
Bytes::from_array(out)
}
///|
pub fn first_clear_color(commands : Array[@core.DrawCommand]) -> @core.Color {
let mut clear = @core.Color::white()
let mut found = false
for command in commands {
if !found {
match command {
@core.DrawCommand::Clear(color) => {
clear = color
found = true
}
_ => ()
}
}
}
clear
}
///|
fn append_rect_vertices(
vertices : Array[RectVertex],
batch : RectBatch,
size : @core.Size,
) -> Unit {
let rect = batch.rect
let color = batch.color
let x0 = pixel_x_to_ndc(rect.origin.x, size.width)
let x1 = pixel_x_to_ndc(rect.origin.x + rect.size.width, size.width)
let y0 = pixel_y_to_ndc(rect.origin.y, size.height)
let y1 = pixel_y_to_ndc(rect.origin.y + rect.size.height, size.height)
push_vertex(vertices, x0, y0, color)
push_vertex(vertices, x0, y1, color)
push_vertex(vertices, x1, y1, color)
push_vertex(vertices, x0, y0, color)
push_vertex(vertices, x1, y1, color)
push_vertex(vertices, x1, y0, color)
}
///|
fn push_vertex(
vertices : Array[RectVertex],
x : Double,
y : Double,
color : @core.Color,
) -> Unit {
vertices.push({ x, y, r: color.r, g: color.g, b: color.b, a: color.a })
}
///|
fn pixel_x_to_ndc(x : Double, width : Double) -> Double {
if width <= 0.0 {
-1.0
} else {
x / width * 2.0 - 1.0
}
}
///|
fn pixel_y_to_ndc(y : Double, height : Double) -> Double {
if height <= 0.0 {
1.0
} else {
1.0 - y / height * 2.0
}
}
///|
fn push_f32le(out : Array[Byte], value : Double) -> Unit {
let bits : UInt = Float::from_double(value)
.reinterpret_as_int()
.reinterpret_as_uint()
out.push((bits & 0xFF).to_byte())
out.push(((bits >> 8) & 0xFF).to_byte())
out.push(((bits >> 16) & 0xFF).to_byte())
out.push(((bits >> 24) & 0xFF).to_byte())
}
///|
pub fn grow_capacity(required : UInt64) -> UInt64 {
let mut capacity = 256UL
while capacity < required {
capacity = capacity * 2UL
}
capacity
}
///|
pub fn size_dim_to_uint(value : Double) -> UInt {
if value < 1.0 {
1U
} else {
value.to_int().reinterpret_as_uint()
}
}