openpencil/tests/engine/render/canvas/tiles/render.test.ts
Danila Poyarkov 379ef8b62d
perf(canvas): rebuild navigation rendering (#591)
* perf(canvas): add traced navigation benchmarks

- Record and replay timestamped pan and zoom gestures through DOM and CDP input paths\n- Correlate input, viewport, render, long-task, and retained-backing events in Chromium traces\n- Report frame pacing, latency, jump, anchor drift, and crisp-settlement metrics

* perf(canvas): stabilize navigation comparisons

- Separate low-overhead metric runs from optional CPU-profile traces\n- Warm scenarios before recording and use a consistent SwiftShader browser configuration\n- Add a canonical momentum-pan reversal gesture alongside pinch reversal

* fix(canvas): require hardware GPU navigation benchmarks

- Run macOS performance captures through Metal-backed ANGLE and reject accidental SwiftShader fallback\n- Record the GL renderer and reserve software GPU mode for portable correctness smoke runs

* perf(canvas): cache shadow rasters for crisp backing

- Rasterize local drop and inner shadows only while constructing retained scene backing\n- Bound native image memory and invalidate cached entries with node and renderer lifecycle changes\n- Quantize zoom-aware raster resolution and reuse nearby scales without lowering normal scene quality

* test(canvas): verify retained shadow raster fidelity

- Compare settled retained-backing shadow output with direct CanvasKit rendering\n- Keep backdrop blur on the picture fallback and exercise graph-driven cache invalidation\n- Cover updates, deletion, and reparenting through actual SceneGraph events

* perf(canvas): benchmark real FIG fixtures

- Serve exact local fixture bytes through an isolated Playwright route for production preview runs\n- Wait for document loading and page population before zooming to fit and recording navigation\n- Record the resolved fixture path in benchmark environment artifacts

* fix(canvas): preserve nested effect subtree pictures

- Keep deeply nested shadow documents on one retained subtree picture instead of exploding them into per-node image draws\n- Restrict shadow raster acceleration to effect-bearing page children\n- Cover nested shadow fallback and restore gold-preview FIG pinch performance to master levels

* refactor(canvas): share recorded wheel sample type

* perf(canvas): defer backing settlement across zoom reversals

- Track explicit navigation phases and gesture generations instead of inferring idle from viewport timing\n- Cancel or defer retained backing construction while pan, zoom, momentum, or tentative settlement is active\n- Add a repeated short-pause pinch reversal fixture based on the user trace

* perf(canvas): index bounded render chunks

- Split oversized painter subtrees into self-paint and bounded descendant chunks without dropping container visuals\n- Bulk-load chunk visual bounds into RBush for selective world-space queries\n- Cover bounded updates and gold-preview build/query complexity before tile rendering consumes the index

* refactor(canvas): namespace render chunk coverage

* perf(canvas): model chunk paint context

- Preserve ancestor transform and clip dependencies for independently renderable chunks\n- Keep opacity, blend, blur, and mask isolation subtrees atomic until command-level splitting exists\n- Report oversized atomic chunks and lock gold-preview to bounded painter units

* perf(canvas): record pixel-correct render chunks

- Record interruptible chunks in world coordinates with ancestor transforms, clips, and chunk-local culling bounds\n- Draw opacity, blend, blur, and mask isolation chunks directly into destination surfaces in painter order\n- Compare composited chunk output with direct CanvasKit rendering instead of relaxing visual thresholds

* perf(canvas): render selective world tiles

- Map world regions to fixed 256-device-pixel tile targets and quantized sharpness levels\n- Query only intersecting render chunks and preserve atomic destination compositing\n- Match multi-tile CanvasKit output against direct rendering and measure gold-preview tile cost

* perf(canvas): cache chunk pictures across tiles

- Reuse world-space chunk command pictures for every intersecting tile\n- Pool 256-pixel tile surfaces and expose allocation, draw, flush, and snapshot timings\n- Keep expensive atomic foreground blur visible as an over-budget scheduler constraint

* perf(canvas): schedule cached tile rendering

- Bound tile images with an LRU cache and reuse pooled CanvasKit surfaces\n- Plan mandatory holes, stale visible refreshes, and overscan by navigation and content generation\n- Stop jobs at a strict deadline while reporting fallbacks, stale work, overruns, and over-budget effects

* perf(canvas): integrate progressive tiled rendering

- Keep retained scene output as the interaction fallback while exact tiles refine only after navigation becomes idle
- Centralize runtime URL flags and pass renderer selection through the typed Vue canvas API
- Replace benchmark sleeps with explicit mode-aware renderer settlement and report exact tiled coverage
- Preserve bounded scheduler metrics, generation cancellation, native resource cleanup, and shared visual-bounds logic

* refactor(app): centralize runtime query configuration

- Parse collaboration, recent-files, benchmark, presentation, and renderer flags in one typed app module
- Remove ad hoc URL parsing from workspace and collaboration runtime consumers
- Cover supported values and production-safe defaults without adding a repository lint rule

* fix(canvas): replace fallback pixels with exact tiles

- Render opaque page-background tile cells and install them with source replacement instead of double-compositing translucent scene content
- Exercise the live progressive controller against direct rendering across masks, effects, blend isolation, images, fallback text, transforms, and clipping
- Preserve the bounded reversal path with zero Long Tasks and exact settlement near 128 ms on gold-preview.fig

* perf(canvas): invalidate tiled content selectively

- Index chunk dependencies across contained nodes and transform or clipping ancestors
- Re-record affected chunk pictures and invalidate tiles intersecting old or new visual bounds
- Advance unaffected cached tiles to the new scene generation instead of rebuilding the full chunk index and tile cache
- Keep structural graph mutations on the safe full-rebuild path and cover selective refresh end to end

* perf(canvas): bound atomic blur tile refresh

- Render atomic blur chunks with tile-local isolation bounds and blur halos instead of replaying full-subtree layers
- Keep content refresh behind the retained fallback, cap GPU submissions to four tile jobs per frame, and adapt estimates from measured work
- Preserve large-radius CPU over-budget visibility while preventing Metal-backed refresh bursts and deferred GPU overload
- Add deterministic node-mutation benchmarks and summarize scheduler throughput, job duration, overruns, and exact content settlement

* perf(canvas): cancel obsolete tile refresh generations

- Count and report queued jobs removed by content or navigation generation changes
- Add deterministic mutation-then-reversal benchmark support without sleeps
- Assert exact tile work remains suspended during navigation and resumes for the final viewport
- Summarize cancellation alongside scheduler throughput, overruns, and settlement metrics

* test(canvas): cover live tiled blur settlement

- Load gold-preview.fig through the real tiled canvas surface and wait on explicit renderer settlement
- Commit the settled radius-210 large-blur browser snapshot
- Replay the canonical zoom reversal during refresh and require byte-identical final canvas convergence

* fix(canvas): harden renderer resource lifecycle

- Release tiled surfaces, images, pictures, and queued work across surface, font, graph, page, structure, and renderer lifecycle boundaries
- Restore pooled canvas, viewport, and backing state through exception-safe native recording and raster paths
- Rebuild tiled chunk topology only when isolation requirements actually change, preserving selective blur mutation performance
- Document deterministic active-renderer settlement and add lifecycle, graph replacement, cache failure, and surface replacement regressions

* test(canvas): remove source-matching renderer claims

- Delete the autopsy suite that inferred runtime correctness from source text, regexes, line placement, and symbol counts
- Keep renderer ordering, cache cleanup, effect behavior, and pixel fidelity covered by executable behavioral and lifecycle tests

* perf(canvas): present retained backing during tiled navigation

- Profile production reversal traces and attribute tiled p95 cost to GPU command-buffer flushes from full-scene fallback replay and tile presentation
- Use the retained backing as the moving fallback while tile scheduling and cached lookup remain allocation-free
- Defer tile image presentation until idle and expose visible versus presented tile counts in navigation telemetry
- Reduce tiled reversal render p95 from about 8ms to 0.3ms while preserving exact idle replacement and visual parity

* perf(canvas): prioritize visible tile settlement

- Profile per-tile allocation, draw, flush, snapshot, and chunk costs through scheduler telemetry
- Defer overscan until all visible exact tiles are covered
- Replace the four-job idle cap with a higher safety ceiling while the measured five-millisecond deadline controls cheap work
- Reduce mutation-plus-reversal exact settlement from about 272ms to 160ms without Long Tasks, overruns, or over-budget jobs

* refactor(canvas): clarify renderer lifecycle boundaries

- Extract retained backing state types and navigation preview timing\n- Isolate tiled scheduler telemetry from frame orchestration\n- Document settlement and CanvasKit ownership invariants\n- Preserve hot drawing loops, budgets, cache limits, and rendering decisions

* fix(canvas): preserve current label rendering

Retain the merged paragraph-label cache lifecycle and substituted-font readiness while reconstructing the renderer stack on current master.

* test(canvas): keep tile benchmark assertions deterministic

Keep performance timing in benchmark telemetry while asserting structural tile selectivity and cache behavior in CI.

* feat(canvas): expose experimental tiled rendering

- Persist retained or tiled canvas mode in General settings\n- Keep retained rendering as the default and apply changes after reload\n- Preserve URL overrides for deterministic benchmarks and support reproduction

* refactor(app): centralize renderer preference state

Expose renderer override provenance from runtime configuration and keep the settings control's derived state separate from its explicit persistence action.

* refactor(app): share settings layout anatomy

Reuse slot-based section headers and bordered groups while keeping each settings control row explicit.

* fix(canvas): harden tiled renderer boundaries

- Bound low-zoom tile planning and handle failed tile surface allocation\n- Preserve effect raster dependencies, runtime-safe clocks, and navigation timing contracts\n- Keep benchmarks deterministic, backward compatible, and accurately localized

* fix(canvas): invalidate dependent node pictures

Track first-child shadow dependencies for retained node pictures so child geometry updates cannot leave stale parent shadows.
2026-08-31 13:39:40 +03:00

451 lines
14 KiB
TypeScript

import { beforeAll, describe, expect, test } from 'bun:test'
import { SceneGraph } from '@open-pencil/scene-graph'
import { initCanvasKit } from '#cli/headless'
import { SkiaRenderer } from '#core/canvas'
import { RenderChunkIndex, RenderChunkPictureCache } from '#core/canvas/renderer/chunks'
import {
deleteRenderedTile,
renderTile,
TiledSceneController,
TILE_DEVICE_SIZE,
TileSurfacePool,
tileKeysForWorldBounds,
tileWorldBounds
} from '#core/canvas/renderer/tiles'
import { expectDefined } from '#tests/helpers/assert'
let ck: Awaited<ReturnType<typeof initCanvasKit>>
beforeAll(async () => {
ck = await initCanvasKit()
})
function color(r: number, g: number, b: number) {
return [{ type: 'SOLID' as const, color: { r, g, b, a: 1 }, opacity: 1, visible: true }]
}
function pixels(image: ReturnType<typeof ck.MakeImageFromEncoded>, width: number, height: number) {
if (!image) throw new Error('Expected image')
return expectDefined(
image.readPixels(0, 0, {
width,
height,
colorType: ck.ColorType.RGBA_8888,
alphaType: ck.AlphaType.Unpremul,
colorSpace: ck.ColorSpace.SRGB
}),
'tile pixels'
)
}
function differenceRatio(a: Uint8Array, b: Uint8Array, tolerance = 8) {
let different = 0
for (let index = 0; index < a.length; index++) {
if (Math.abs(a[index] - b[index]) > tolerance) different++
}
return different / a.length
}
function squareCommandsBlob(): Uint8Array {
const blob = new Uint8Array(1 + 4 * 9 + 1)
const view = new DataView(blob.buffer)
let offset = 0
for (const [command, x, y] of [
[1, 0, 0],
[2, 1, 0],
[2, 1, 1],
[2, 0, 1]
] as const) {
blob[offset] = command
view.setFloat32(offset + 1, x, true)
view.setFloat32(offset + 5, y, true)
offset += 9
}
blob[offset] = 0
return blob
}
function testImageBytes(): Uint8Array {
const surface = expectDefined(ck.MakeSurface(8, 8), 'test image surface')
const canvas = surface.getCanvas()
canvas.clear(ck.Color4f(0.95, 0.75, 0.1, 1))
const paint = new ck.Paint()
paint.setColor(ck.Color4f(0.1, 0.3, 0.95, 1))
canvas.drawRect(ck.LTRBRect(0, 0, 4, 8), paint)
surface.flush()
const image = surface.makeImageSnapshot()
const bytes = expectDefined(image.encodeToBytes(ck.ImageFormat.PNG, 100), 'test image PNG')
image.delete()
paint.delete()
surface.delete()
return bytes
}
function createParityGraph() {
const graph = new SceneGraph()
const page = expectDefined(graph.getPages()[0], 'page')
const clip = graph.createNode('FRAME', page.id, {
x: 40,
y: 30,
width: 260,
height: 180,
clipsContent: true,
cornerRadius: 20,
fills: color(0.95, 0.95, 0.98)
})
const frame = graph.createNode('FRAME', clip.id, { width: 360, height: 160, fills: [] })
for (let index = 0; index < 48; index++) {
graph.createNode(index % 2 === 0 ? 'RECTANGLE' : 'ELLIPSE', frame.id, {
x: (index % 12) * 28,
y: Math.floor(index / 12) * 36,
width: 32,
height: 32,
fills: color((index % 3) * 0.35, 0.3, 0.85 - (index % 2) * 0.3)
})
}
const translucent = graph.createNode('FRAME', page.id, {
x: 210,
y: 55,
width: 100,
height: 120,
opacity: 0.55,
blendMode: 'MULTIPLY',
fills: [],
effects: [
{
type: 'FOREGROUND_BLUR',
visible: true,
radius: 12,
spread: 0,
offset: { x: 0, y: 0 },
color: { r: 0, g: 0, b: 0, a: 1 }
}
]
})
graph.createNode('RECTANGLE', translucent.id, {
x: 5,
y: 5,
width: 80,
height: 90,
fills: color(0.9, 0.2, 0.4),
effects: [
{
type: 'DROP_SHADOW',
visible: true,
color: { r: 0.1, g: 0.1, b: 0.2, a: 0.7 },
offset: { x: 18, y: 6 },
radius: 16,
spread: 3
}
]
})
const masked = graph.createNode('FRAME', page.id, {
x: 105,
y: 150,
width: 130,
height: 80,
fills: []
})
graph.createNode('ELLIPSE', masked.id, {
x: 0,
y: 0,
width: 90,
height: 70,
isMask: true,
fills: color(0, 0, 0)
})
graph.createNode('RECTANGLE', masked.id, {
x: -15,
y: -5,
width: 150,
height: 90,
fills: color(0.1, 0.8, 0.35)
})
const imageHash = 'tile-parity-image'
graph.images.set(imageHash, testImageBytes())
graph.createNode('RECTANGLE', page.id, {
x: 238,
y: 176,
width: 68,
height: 52,
rotation: -7,
fills: [
{
type: 'IMAGE',
imageHash,
imageScaleMode: 'FILL',
opacity: 1,
visible: true
}
]
})
graph.createNode('TEXT', page.id, {
x: 245,
y: 15,
width: 64,
height: 34,
rotation: 9,
text: 'tile',
fontFamily: '__MissingFont__',
fontSize: 22,
fills: color(0.15, 0.1, 0.75),
derivedTextGlyphs: [
{ commandsBlob: squareCommandsBlob(), x: 0, y: 22, fontSize: 18 },
{ commandsBlob: squareCommandsBlob(), x: 20, y: 22, fontSize: 18 },
{ commandsBlob: squareCommandsBlob(), x: 40, y: 22, fontSize: 18 }
]
})
return { graph, page }
}
describe('tile rendering', () => {
test('restores pooled tile canvas transforms before the surface is reused', () => {
const graph = new SceneGraph()
const page = expectDefined(graph.getPages()[0], 'page')
graph.createNode('RECTANGLE', page.id, {
x: 20,
y: 20,
width: 80,
height: 80,
fills: color(1, 0, 0)
})
const surface = expectDefined(ck.MakeSurface(320, 240), 'renderer surface')
const renderer = new SkiaRenderer(ck, surface)
renderer.pageColor = { r: 1, g: 1, b: 1, a: 1 }
const { index } = RenderChunkIndex.build(graph, page.id)
const pictureCache = new RenderChunkPictureCache()
const surfacePool = new TileSurfacePool()
const key = { pageId: page.id, level: 1, x: 0, y: 0 }
try {
const first = expectDefined(
renderTile(renderer, graph, index, key, pictureCache, surfacePool),
'first tile'
)
const second = expectDefined(
renderTile(renderer, graph, index, key, pictureCache, surfacePool),
'second tile'
)
const firstPixels = pixels(first.image, TILE_DEVICE_SIZE, TILE_DEVICE_SIZE)
const secondPixels = pixels(second.image, TILE_DEVICE_SIZE, TILE_DEVICE_SIZE)
expect(firstPixels.every((value, index) => value === secondPixels[index])).toBe(true)
deleteRenderedTile(first)
deleteRenderedTile(second)
} finally {
surfacePool.clear()
pictureCache.clear()
index.dispose()
renderer.destroy()
}
})
test('cancels obsolete refresh jobs and performs no exact work during navigation', () => {
const graph = new SceneGraph()
const page = expectDefined(graph.getPages()[0], 'page')
for (let index = 0; index < 48; index++) {
graph.createNode('RECTANGLE', page.id, {
x: (index % 12) * 100,
y: Math.floor(index / 12) * 100,
width: 80,
height: 80,
fills: color(0.2, 0.4, 0.8)
})
}
const surface = expectDefined(ck.MakeSurface(1280, 800), 'cancellation surface')
const renderer = new SkiaRenderer(ck, surface)
const controller = new TiledSceneController()
renderer.pageId = page.id
renderer.pageColor = { r: 1, g: 1, b: 1, a: 1 }
renderer.viewportWidth = 1280
renderer.viewportHeight = 800
renderer.dpr = 1
renderer.zoom = 1
renderer.navigationPhase = 'idle'
try {
const initial = controller.renderFrame(renderer, surface.getCanvas(), graph, 1, 0)
expect(initial.pending).toBe(true)
renderer.navigationPhase = 'zoom'
renderer.navigationGeneration = 1
const navigating = controller.renderFrame(renderer, surface.getCanvas(), graph, 1, 1)
expect(navigating.metrics.cancelledJobs).toBeGreaterThan(0)
expect(navigating.metrics.mandatoryCompleted).toBe(0)
expect(navigating.metrics.interruptibleCompleted).toBe(0)
} finally {
controller.destroy()
renderer.destroy()
}
})
test('refreshes only tiles intersecting an updated isolated chunk', () => {
const graph = new SceneGraph()
const page = expectDefined(graph.getPages()[0], 'page')
const changed = graph.createNode('RECTANGLE', page.id, {
x: 20,
y: 20,
width: 40,
height: 40,
fills: color(1, 0, 0)
})
graph.createNode('RECTANGLE', page.id, {
x: 540,
y: 20,
width: 40,
height: 40,
fills: color(0, 0, 1)
})
const surface = expectDefined(ck.MakeSurface(640, 240), 'selective invalidation surface')
const renderer = new SkiaRenderer(ck, surface)
const controller = new TiledSceneController()
renderer.pageId = page.id
renderer.pageColor = { r: 1, g: 1, b: 1, a: 1 }
renderer.viewportWidth = 640
renderer.viewportHeight = 240
renderer.dpr = 1
renderer.zoom = 1
renderer.navigationPhase = 'idle'
try {
let initial = controller.renderFrame(renderer, surface.getCanvas(), graph, 1, 0)
for (let frame = 0; frame < 10 && initial.pending; frame++) {
initial = controller.renderFrame(renderer, surface.getCanvas(), graph, 1, 0)
}
expect(initial.covered).toBe(true)
controller.invalidateNode(changed.id)
graph.updateNode(changed.id, { x: 30, fills: color(0, 1, 0) })
const refreshed = controller.renderFrame(renderer, surface.getCanvas(), graph, 2, 0)
expect(refreshed.metrics.interruptibleCompleted).toBe(1)
expect(refreshed.covered).toBe(true)
} finally {
controller.destroy()
renderer.destroy()
}
})
test('live controller progressively replaces fallback pixels without changing the scene', () => {
const { graph, page } = createParityGraph()
const directSurface = expectDefined(ck.MakeSurface(320, 240), 'direct controller surface')
const tiledSurface = expectDefined(ck.MakeSurface(320, 240), 'tiled controller surface')
const direct = new SkiaRenderer(ck, directSurface)
const tiled = new SkiaRenderer(ck, tiledSurface)
const controller = new TiledSceneController()
const pageColor = { r: 1, g: 1, b: 1, a: 1 }
for (const renderer of [direct, tiled]) {
renderer.pageId = page.id
renderer.pageColor = pageColor
renderer.viewportWidth = 320
renderer.viewportHeight = 240
renderer.dpr = 1
renderer.zoom = 1
renderer.navigationPhase = 'idle'
renderer.nodeFontReadiness = () => 'exhausted'
}
try {
direct.surface.getCanvas().clear(ck.WHITE)
direct.renderSceneToCanvas(direct.surface.getCanvas(), graph, page.id)
direct.surface.flush()
const directImage = direct.surface.makeImageSnapshot()
let result = { covered: false, pending: true }
for (let frame = 0; frame < 20 && result.pending; frame++) {
const canvas = tiled.surface.getCanvas()
canvas.clear(ck.WHITE)
tiled.renderSceneToCanvas(canvas, graph, page.id)
result = controller.renderFrame(tiled, canvas, graph, 1, 0)
tiled.surface.flush()
}
expect(result.covered).toBe(true)
expect(result.pending).toBe(false)
const tiledImage = tiled.surface.makeImageSnapshot()
expect(
differenceRatio(pixels(directImage, 320, 240), pixels(tiledImage, 320, 240))
).toBeLessThan(0.01)
directImage.delete()
tiledImage.delete()
} finally {
controller.destroy()
direct.destroy()
tiled.destroy()
}
})
test('composes multiple queried tiles to match direct scene rendering', () => {
const { graph, page } = createParityGraph()
const directSurface = expectDefined(ck.MakeSurface(320, 240), 'direct surface')
const tiledSurface = expectDefined(ck.MakeSurface(320, 240), 'tiled surface')
const tileFactorySurface = expectDefined(ck.MakeSurface(320, 240), 'tile factory surface')
const direct = new SkiaRenderer(ck, directSurface)
const tiled = new SkiaRenderer(ck, tiledSurface)
const tileFactory = new SkiaRenderer(ck, tileFactorySurface)
direct.pageColor = { r: 1, g: 1, b: 1, a: 1 }
tiled.pageColor = { r: 1, g: 1, b: 1, a: 1 }
tileFactory.pageColor = { r: 1, g: 1, b: 1, a: 1 }
direct.nodeFontReadiness = () => 'exhausted'
tiled.nodeFontReadiness = () => 'exhausted'
tileFactory.nodeFontReadiness = () => 'exhausted'
const { index } = RenderChunkIndex.build(graph, page.id)
const level = 1
const keys = tileKeysForWorldBounds(page.id, level, {
minX: 0,
minY: 0,
maxX: 320,
maxY: 240
})
const pictureCache = new RenderChunkPictureCache()
const surfacePool = new TileSurfacePool()
try {
direct.surface.getCanvas().clear(ck.WHITE)
direct.renderSceneToCanvas(direct.surface.getCanvas(), graph, page.id)
direct.surface.flush()
const directImage = direct.surface.makeImageSnapshot()
const tiledCanvas = tiled.surface.getCanvas()
tiledCanvas.clear(ck.WHITE)
const rendered = keys.map((key) =>
expectDefined(
renderTile(tileFactory, graph, index, key, pictureCache, surfacePool),
'rendered tile'
)
)
const tilePaint = new ck.Paint()
tilePaint.setBlendMode(ck.BlendMode.Src)
for (const tile of rendered) {
const bounds = tileWorldBounds(tile.key)
tiledCanvas.drawImageRectOptions(
tile.image,
ck.LTRBRect(0, 0, TILE_DEVICE_SIZE, TILE_DEVICE_SIZE),
ck.LTRBRect(bounds.minX, bounds.minY, bounds.maxX, bounds.maxY),
ck.FilterMode.Nearest,
ck.MipmapMode.None,
tilePaint
)
}
tilePaint.delete()
tiled.surface.flush()
const tiledImage = tiled.surface.makeImageSnapshot()
expect(
differenceRatio(pixels(directImage, 320, 240), pixels(tiledImage, 320, 240))
).toBeLessThan(0.01)
expect(rendered.every((tile) => tile.chunkCount < index.size())).toBe(true)
expect(rendered.every((tile) => tile.image.width() === TILE_DEVICE_SIZE)).toBe(true)
directImage.delete()
tiledImage.delete()
for (const tile of rendered) deleteRenderedTile(tile)
} finally {
surfacePool.clear()
pictureCache.clear()
index.dispose()
direct.destroy()
tiled.destroy()
tileFactory.destroy()
}
})
})