Files
prosperon/compositor.cm
2025-12-29 20:46:42 -06:00

596 lines
16 KiB
Plaintext

// compositor.cm - Unified Compositor (Rewritten)
//
// The compositor is the SINGLE OWNER of all effects.
// It takes scene descriptions and produces abstract render plans.
//
// Architecture:
// Scene Tree (Retained) -> Compositor (Effect orchestration) -> Render Plan -> Backend
//
// The compositor does NOT know about sprites/tilemaps/text - that's renderer territory.
// It only knows about plates, targets, viewports, effects, and post-processing.
var effects_mod = use('effects')
var compositor = {}
// Presentation modes
compositor.PRESENTATION = {
DISABLED: 'disabled',
STRETCH: 'stretch',
LETTERBOX: 'letterbox',
OVERSCAN: 'overscan',
INTEGER_SCALE: 'integer_scale'
}
// Blend modes
compositor.BLEND = {
REPLACE: 'replace',
OVER: 'over',
ADD: 'add'
}
// Compile a composition tree into a render plan
compositor.compile = function(comp, renderers, backend) {
var ctx = {
renderers: renderers,
backend: backend,
passes: [],
targets: {},
persistent_targets: {},
target_counter: 0,
screen_size: null,
target_size: null,
// Target allocation
alloc_target: function(w, h, hint) {
var key = (hint || 'target') + '_' + text(this.target_counter++)
this.targets[key] = {w: w, h: h, key: key}
return {type: 'target', key: key, w: w, h: h}
},
// Persistent target (survives across frames)
get_persistent_target: function(key, w, h) {
if (!this.persistent_targets[key]) {
this.persistent_targets[key] = {w: w, h: h, key: key, persistent: true}
}
return {type: 'target', key: key, w: w, h: h, persistent: true}
}
}
// Get screen size from backend
ctx.screen_size = backend.get_window_size ? backend.get_window_size() : {width: 1280, height: 720}
if (!ctx.screen_size.width) ctx.screen_size.width = 1280
if (!ctx.screen_size.height) ctx.screen_size.height = 720
// Normalize to w/h
ctx.screen_size.w = ctx.screen_size.width
ctx.screen_size.h = ctx.screen_size.height
// Compile the tree
_compile_node(comp, ctx, null, ctx.screen_size)
return {
passes: ctx.passes,
targets: ctx.targets,
persistent_targets: ctx.persistent_targets,
screen_size: ctx.screen_size
}
}
// Compile a single node
function _compile_node(node, ctx, parent_target, parent_size) {
if (!node) return {output: null}
var node_type = node.type
var owns_target = false
var target = parent_target
var target_size = {w: parent_size.w || parent_size.width, h: parent_size.h || parent_size.height}
// Determine target ownership
if (node.target == 'screen') {
owns_target = true
target = 'screen'
target_size = {w: ctx.screen_size.w, h: ctx.screen_size.h}
} else if (node.resolution) {
owns_target = true
target_size = {w: node.resolution.w || node.resolution.width, h: node.resolution.h || node.resolution.height}
target = ctx.alloc_target(target_size.w, target_size.h, node.name || 'res_target')
} else if (node.effects && _effects_require_target(node.effects, ctx)) {
owns_target = true
target = ctx.alloc_target(target_size.w, target_size.h, node.name || 'effect_target')
}
ctx.target_size = target_size
// Handle clear
if (owns_target && node.clear) {
ctx.passes.push({
type: 'clear',
target: target,
color: node.clear
})
}
// Process by type
if (node_type == 'composition') {
return _compile_composition(node, ctx, target, target_size)
} else if (node_type == 'group') {
return _compile_group(node, ctx, target, target_size, parent_target, parent_size)
} else if (_is_renderer_type(node_type)) {
return _compile_renderer(node, ctx, target, target_size, parent_target, parent_size)
} else if (node_type == 'imgui') {
ctx.passes.push({
type: 'imgui',
target: target,
target_size: target_size,
draw: node.draw,
rect: node.rect
})
return {output: target}
}
return {output: target}
}
// Compile composition root
function _compile_composition(node, ctx, target, target_size) {
var layers = node.layers || []
for (var i = 0; i < layers.length; i++) {
var layer = layers[i]
if (!layer.blend) {
layer.blend = (i == 0) ? 'replace' : 'over'
}
_compile_node(layer, ctx, target, target_size)
}
return {output: target}
}
// Compile group with effects
function _compile_group(node, ctx, target, target_size, parent_target, parent_size) {
var layers = node.layers || []
var original_target = target
// Process child layers
for (var i = 0; i < layers.length; i++) {
var layer = layers[i]
if (!layer.blend) {
layer.blend = (i == 0) ? 'replace' : 'over'
}
_compile_node(layer, ctx, target, target_size)
}
// Apply effects - this is where the compositor owns all effect logic
if (node.effects && node.effects.length > 0) {
for (var j = 0; j < node.effects.length; j++) {
var effect = node.effects[j]
target = _compile_effect(effect, ctx, target, target_size, node.name || ('group_' + ctx.target_counter))
}
}
// Composite back to parent if needed
var needs_composite = (original_target != parent_target || target != original_target) && parent_target
if (needs_composite) {
var presentation = node.presentation || 'disabled'
var blend = node.blend || 'over'
if (parent_target == 'screen') {
ctx.passes.push({
type: 'blit_to_screen',
source: target,
source_size: target_size,
dest_size: parent_size,
presentation: presentation,
pos: node.pos
})
} else {
ctx.passes.push({
type: 'composite',
source: target,
dest: parent_target,
source_size: target_size,
dest_size: parent_size,
presentation: presentation,
blend: blend,
pos: node.pos
})
}
}
return {output: target}
}
// Compile renderer layer (film2d, forward3d, etc.)
function _compile_renderer(node, ctx, target, target_size, parent_target, parent_size) {
var renderer_type = node.type
var renderer = ctx.renderers[renderer_type]
if (!renderer) {
log.console(`compositor: Unknown renderer: ${renderer_type}`)
return {output: target}
}
var layer_target = target
var layer_size = target_size
var owns_target = false
// Check for resolution override
if (node.resolution) {
owns_target = true
layer_size = {w: node.resolution.w, h: node.resolution.h}
layer_target = ctx.alloc_target(layer_size.w, layer_size.h, node.name || renderer_type + '_target')
}
// Clear if we own target
if (owns_target && node.clear) {
ctx.passes.push({
type: 'clear',
target: layer_target,
color: node.clear
})
}
// Emit render pass
ctx.passes.push({
type: 'render',
renderer: renderer_type,
root: node.root,
camera: node.camera,
target: layer_target,
target_size: layer_size,
blend: node.blend || 'over',
clear: owns_target ? node.clear : null
})
// Composite back to parent
if (owns_target && parent_target) {
var presentation = node.presentation || 'disabled'
var blend = node.blend || 'over'
ctx.passes.push({
type: 'composite',
source: layer_target,
dest: parent_target,
source_size: layer_size,
dest_size: parent_size,
presentation: presentation,
blend: blend,
pos: node.pos
})
}
return {output: layer_target}
}
// Compile an effect using the effect registry
function _compile_effect(effect, ctx, input_target, target_size, node_id) {
var effect_type = effect.type
var effect_def = effects_mod.get(effect_type)
if (!effect_def) {
log.console(`compositor: Unknown effect: ${effect_type}`)
return input_target
}
// Build effect context
var effect_ctx = {
backend: ctx.backend,
target_size: {w: target_size.w, h: target_size.h},
alloc_target: function(w, h, hint) {
return ctx.alloc_target(w, h, hint)
},
get_persistent_target: function(key, w, h) {
return ctx.get_persistent_target(node_id + '_' + key, w, h)
}
}
// Allocate output target
var output = ctx.alloc_target(target_size.w, target_size.h, effect_type + '_out')
// Build effect passes
var effect_passes = effect_def.build_passes(input_target, output, effect, effect_ctx)
// Convert effect passes to compositor passes
for (var i = 0; i < effect_passes.length; i++) {
var ep = effect_passes[i]
ctx.passes.push(_convert_effect_pass(ep, ctx))
}
return output
}
// Convert effect pass to compositor pass format
function _convert_effect_pass(ep, ctx) {
switch (ep.type) {
case 'shader':
if (!ep.input && !ep.inputs) {
ep.input = ep.inputs[0]
}
return {
type: 'shader_pass',
shader: ep.shader,
input: ep.input || ep.inputs[0],
extra_inputs: ep.inputs ? ep.inputs.slice(1) : [],
output: ep.output,
uniforms: ep.uniforms
}
case 'composite':
return {
type: 'composite_textures',
base: ep.base,
overlay: ep.overlay,
output: ep.output,
mode: ep.blend || 'over'
}
case 'blit':
return {
type: 'composite',
source: ep.source,
dest: ep.dest,
source_size: ep.source.w ? {w: ep.source.w, h: ep.source.h} : ctx.target_size,
dest_size: ep.dest.w ? {w: ep.dest.w, h: ep.dest.h} : ctx.target_size,
presentation: 'disabled'
}
case 'render_subtree':
return {
type: 'render_mask_source',
source: ep.root,
target: ep.output,
target_size: {w: ep.output.w, h: ep.output.h},
space: ep.space || 'local'
}
default:
return ep
}
}
// Check if effects require a target
function _effects_require_target(effects, ctx) {
for (var i = 0; i < effects.length; i++) {
var effect = effects[i]
if (effects_mod.requires_target(effect.type)) return true
}
return false
}
// Check if type is a renderer
function _is_renderer_type(type) {
return type == 'film2d' || type == 'forward3d' || type == 'imgui' || type == 'retro3d' || type == 'simple2d'
}
// Calculate presentation rect
compositor.calculate_presentation_rect = function(source_size, dest_size, mode) {
var sw = source_size.w || source_size.width
var sh = source_size.h || source_size.height
var dw = dest_size.w || dest_size.width
var dh = dest_size.h || dest_size.height
if (mode == 'disabled') {
return {x: 0, y: 0, width: number.min(sw, dw), height: number.min(sh, dh)}
}
if (mode == 'stretch') {
return {x: 0, y: 0, width: dw, height: dh}
}
var src_aspect = sw / sh
var dst_aspect = dw / dh
if (mode == 'letterbox') {
var scale = src_aspect > dst_aspect ? dw / sw : dh / sh
var w = sw * scale
var h = sh * scale
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
}
if (mode == 'overscan') {
var scale = src_aspect > dst_aspect ? dh / sh : dw / sw
var w = sw * scale
var h = sh * scale
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
}
if (mode == 'integer_scale') {
var scale_x = number.floor(dw / sw)
var scale_y = number.floor(dh / sh)
var scale = number.max(1, number.min(scale_x, scale_y))
var w = sw * scale
var h = sh * scale
return {x: (dw - w) / 2, y: (dh - h) / 2, width: w, height: h}
}
return {x: 0, y: 0, width: sw, height: sh}
}
// Execute a compiled render plan
compositor.execute = function(plan, renderers, backend) {
var target_cache = {}
// Pre-allocate targets
for (var key in plan.targets) {
var spec = plan.targets[key]
target_cache[key] = backend.get_or_create_target(spec.w, spec.h, key)
}
for (var key in plan.persistent_targets) {
var spec = plan.persistent_targets[key]
if (!target_cache[key]) {
target_cache[key] = backend.get_or_create_target(spec.w, spec.h, key)
}
}
// Resolve target references
function resolve_target(t) {
if (t == 'screen') return 'screen'
if (t && t.type == 'target') return target_cache[t.key]
return t
}
// Execute passes
var commands = []
for (var i = 0; i < plan.passes.length; i++) {
var pass = plan.passes[i]
var pass_cmds = _execute_pass(pass, renderers, backend, resolve_target, compositor)
for (var j = 0; j < pass_cmds.length; j++) {
commands.push(pass_cmds[j])
}
}
return {commands: commands}
}
// Execute a single pass
function _execute_pass(pass, renderers, backend, resolve_target, comp) {
var commands = []
var source
switch (pass.type) {
case 'clear':
var target = resolve_target(pass.target)
commands.push({cmd: 'begin_render', target: target, clear: pass.color})
commands.push({cmd: 'end_render'})
break
case 'render':
var renderer = renderers[pass.renderer]
if (renderer && renderer.render) {
var target = resolve_target(pass.target)
var result = renderer.render({
root: pass.root,
camera: pass.camera,
target: target,
target_size: pass.target_size,
blend: pass.blend,
clear: pass.clear
}, backend)
if (result && result.commands) {
for (var i = 0; i < result.commands.length; i++) {
commands.push(result.commands[i])
}
}
}
break
case 'shader_pass':
var input = resolve_target(pass.input)
var output = resolve_target(pass.output)
var extra = []
if (pass.extra_inputs) {
for (var i = 0; i < pass.extra_inputs.length; i++) {
extra.push(resolve_target(pass.extra_inputs[i]))
}
}
commands.push({
cmd: 'shader_pass',
shader: pass.shader,
input: input,
output: output,
extra_inputs: extra,
uniforms: pass.uniforms
})
break
case 'composite': {
source = resolve_target(pass.source)
var dest = resolve_target(pass.dest)
var rect = comp.calculate_presentation_rect(pass.source_size, pass.dest_size, pass.presentation)
if (pass.pos) {
rect.x += (pass.pos.x || 0)
rect.y += (pass.pos.y || 0)
}
var filter = pass.presentation == 'integer_scale' ? 'nearest' : 'linear'
commands.push({
cmd: 'blit',
texture: source,
target: dest,
dst_rect: rect,
filter: filter
})
}
break
case 'blit_to_screen': {
source = resolve_target(pass.source)
var rect = comp.calculate_presentation_rect(pass.source_size, pass.dest_size, pass.presentation)
if (pass.pos) {
rect.x += (pass.pos.x || 0)
rect.y += (pass.pos.y || 0)
}
var filter = pass.presentation == 'integer_scale' ? 'nearest' : 'linear'
commands.push({
cmd: 'blit',
texture: source,
target: 'screen',
dst_rect: rect,
filter: filter
})
}
break
case 'composite_textures': {
var base = resolve_target(pass.base)
var overlay = resolve_target(pass.overlay)
var output = resolve_target(pass.output)
commands.push({
cmd: 'composite_textures',
base: base,
overlay: overlay,
output: output,
mode: pass.mode || 'over'
})
}
break
case 'apply_mask': {
var content = resolve_target(pass.content)
var mask = resolve_target(pass.mask)
var output = resolve_target(pass.output)
commands.push({
cmd: 'apply_mask',
content_texture: content,
mask_texture: mask,
output: output,
mode: pass.mode,
invert: pass.invert
})
}
break
case 'imgui': {
commands.push({
cmd: 'imgui',
draw: pass.draw,
rect: pass.rect,
target: resolve_target(pass.target)
})
}
break
case 'render_mask_source': {
var target = resolve_target(pass.target)
var renderer = renderers['film2d']
if (renderer && renderer.render) {
var result = renderer.render({
root: pass.source,
camera: {pos: [0, 0], width: pass.target_size.w, height: pass.target_size.h, anchor: [0, 0], ortho: true},
target: target,
target_size: pass.target_size,
blend: 'replace',
clear: {r: 0, g: 0, b: 0, a: 0}
}, backend)
if (result && result.commands) {
for (var i = 0; i < result.commands.length; i++) {
commands.push(result.commands[i])
}
}
}
}
break
}
return commands
}
return compositor