From 4fcd40da98cd9864cd0f0a5dc8c7ad02cca0682b Mon Sep 17 00:00:00 2001 From: Jamie Wong Date: Sun, 28 Jan 2018 03:13:12 -0800 Subject: [PATCH] Row atlas sort of working? --- canvas-context.ts | 11 +- dev.html | 153 ++++++++++++++++ flamechart-minimap-view.tsx | 2 +- flamechart-renderer.ts | 341 +++++++++++++++++++++--------------- flamechart-view.tsx | 8 +- lru-cache.ts | 152 ++++++++++++++++ math.ts | 13 +- package.json | 2 +- texture-catched-renderer.ts | 173 +++++++++--------- 9 files changed, 611 insertions(+), 244 deletions(-) create mode 100644 lru-cache.ts diff --git a/canvas-context.ts b/canvas-context.ts index 46b5cbe..a2accdb 100644 --- a/canvas-context.ts +++ b/canvas-context.ts @@ -18,6 +18,7 @@ export class CanvasContext { private viewportRectangleRenderer: ViewportRectangleRenderer private textureRenderer: TextureRenderer private setViewportScope: regl.Command<{ physicalBounds: Rect }> + private setScissor: regl.Command<{}> constructor(canvas: HTMLCanvasElement) { this.gl = regl({ @@ -33,7 +34,7 @@ export class CanvasContext { this.rectangleBatchRenderer = new RectangleBatchRenderer(this.gl) this.viewportRectangleRenderer = new ViewportRectangleRenderer(this.gl) this.textureRenderer = new TextureRenderer(this.gl) - + this.setScissor = this.gl({ scissor: { enable: true } }) this.setViewportScope = this.gl({ context: { viewportX: (context: regl.Context, props: SetViewportScopeProps) => { @@ -87,9 +88,7 @@ export class CanvasContext { private statsPanel: StatsPanel | null = this.perfDebug ? new StatsPanel() : null private onBeforeFrame = (context: regl.Context) => { - this.gl({ - scissor: { enable: false } - })(() => { + this.setScissor(() => { this.gl.clear({ color: [0, 0, 0, 0] }) }) @@ -120,9 +119,7 @@ export class CanvasContext { } drawTexture(props: TextureRendererProps) { - this.gl({})((context: regl.Context) => { - this.textureRenderer.render(context, props) - }) + this.textureRenderer.render(props) } createRectangleBatch(): RectangleBatch { diff --git a/dev.html b/dev.html index 05f2141..01444dd 100644 --- a/dev.html +++ b/dev.html @@ -5,6 +5,159 @@ speedscope + diff --git a/flamechart-minimap-view.tsx b/flamechart-minimap-view.tsx index ba3fc81..d6f53a4 100644 --- a/flamechart-minimap-view.tsx +++ b/flamechart-minimap-view.tsx @@ -99,7 +99,7 @@ export class FlamechartMinimapView extends Component { - this.cachedRenderer!.render(context, { + this.cachedRenderer!.render({ physicalSize: this.physicalViewSize() }) this.props.canvasContext.drawViewportRectangle({ diff --git a/flamechart-renderer.ts b/flamechart-renderer.ts index cd3bdb5..6fe584b 100644 --- a/flamechart-renderer.ts +++ b/flamechart-renderer.ts @@ -3,15 +3,99 @@ import { Flamechart } from './flamechart' import { RectangleBatch } from './rectangle-batch-renderer' import { CanvasContext } from './canvas-context'; import { Vec2, Rect, AffineTransform } from './math' +import { LRUCache } from './lru-cache' const MAX_BATCH_SIZE = 10000 +class RowAtlas { + private texture: regl.Texture + private framebuffer: regl.Framebuffer + private renderToFramebuffer: regl.Command<{}> + private rowCache: LRUCache + + constructor(private canvasContext: CanvasContext) { + this.texture = canvasContext.gl.texture({ + width: Math.min(canvasContext.getMaxTextureSize(), 4096), + height: Math.min(canvasContext.getMaxTextureSize(), 4096), + wrapS: 'clamp', + wrapT: 'clamp', + }) + this.framebuffer = canvasContext.gl.framebuffer({ color: [this.texture] }) + this.rowCache = new LRUCache(this.texture.height) + this.renderToFramebuffer = canvasContext.gl({ + framebuffer: this.framebuffer + }) + } + + has(key: K) { return this.rowCache.has(key) } + getCapacity() { return this.texture.height } + + private allocateLine(key: K): number { + if (this.rowCache.getSize() < this.rowCache.getCapacity()) { + // Not in cache, but cache isn't full + const row = this.rowCache.getSize() + this.rowCache.insert(key, row) + return row + } else { + // Not in cache, and cache is full. Evict something. + const [, row] = this.rowCache.removeLRU()! + this.rowCache.insert(key, row) + return row + } + } + + writeToAtlasIfNeeded( + keys: K[], + render: (textureDstRect: Rect, key: K) => void + ) { + this.renderToFramebuffer((context: regl.Context) => { + for (let key of keys) { + let row = this.rowCache.get(key) + if (row != null) { + // Already cached! + return + } + + // Not cached -- we'll have to actually render + row = this.allocateLine(key) + const textureRect = new Rect( + new Vec2(0, row), + new Vec2(this.texture.width, 1) + ) + + render(textureRect, key) + } + }) + return + } + + renderViaAtlas(key: K, dstRect: Rect): boolean { + let row = this.rowCache.get(key) + if (row == null) { + return false + } + + const textureRect = new Rect( + new Vec2(0, row), + new Vec2(this.texture.width, 1) + ) + + // At this point, we have the row in cache, and we can + // paint directly from it into the framebuffer. + this.canvasContext.drawTexture({ + texture: this.texture, + srcRect: textureRect, + dstRect: dstRect + }) + return true + } +} + interface RangeTreeNode { - getMinLeft(): number - getMaxRight(): number + getBounds(): Rect getRectCount(): number getChildren(): RangeTreeNode[] - forEachBatchWithinBounds(configSpaceViewport: Rect, cb: (batch: RectangleBatch) => void): void + forEachLeafNodeWithinBounds(configSpaceBounds: Rect, cb: (leaf: RangeTreeLeafNode) => void): void } class RangeTreeLeafNode implements RangeTreeNode { @@ -19,46 +103,54 @@ class RangeTreeLeafNode implements RangeTreeNode { constructor( private batch: RectangleBatch, - private minLeft: number, - private maxRight: number - ) {} + private bounds: Rect + ) { + batch.uploadToGPU() + } - getMinLeft() { return this.minLeft } - getMaxRight() { return this.maxRight } + getBatch() { return this.batch } + getBounds() { return this.bounds } getRectCount() { return this.batch.getRectCount() } getChildren() { return this.children } - forEachBatchWithinBounds(configSpaceViewport: Rect, cb: (batch: RectangleBatch) => void) { - if (this.maxRight < configSpaceViewport.left()) return - if (this.minLeft > configSpaceViewport.right()) return - cb(this.batch) - - // TODO(jlfwong): Remove this line. This is here for now to artificially - // decrease rendering performance to try various optimizations. - cb(this.batch) + forEachLeafNodeWithinBounds(configSpaceBounds: Rect, cb: (leaf: RangeTreeLeafNode) => void) { + if (!this.bounds.hasIntersectionWith(configSpaceBounds)) return + cb(this) } } class RangeTreeInteriorNode implements RangeTreeNode { private rectCount: number = 0 + private bounds: Rect constructor(private children: RangeTreeNode[]) { if (children.length === 0) { throw new Error("Empty interior node") } + let minLeft = Infinity + let maxRight = -Infinity + let minTop = Infinity + let maxBottom = -Infinity for (let child of children) { this.rectCount += child.getRectCount() + const bounds = child.getBounds() + minLeft = Math.min(minLeft, bounds.left()) + maxRight = Math.max(maxRight, bounds.right()) + minTop = Math.min(minTop, bounds.top()) + maxBottom = Math.max(maxBottom, bounds.bottom()) } + this.bounds = new Rect( + new Vec2(minLeft, minTop), + new Vec2(maxRight - minLeft, maxBottom - minTop) + ) } - getMinLeft() { return this.children[0].getMinLeft() } - getMaxRight() { return this.children[this.children.length - 1].getMaxRight() } + getBounds() { return this.bounds } getRectCount() { return this.rectCount } getChildren() { return this.children } - forEachBatchWithinBounds(configSpaceViewport: Rect, cb: (batch: RectangleBatch) => void) { - // if (this.getMaxRight() < configSpaceViewport.left()) return - // if (this.getMinLeft() > configSpaceViewport.right()) return + forEachLeafNodeWithinBounds(configSpaceBounds: Rect, cb: (leaf: RangeTreeLeafNode) => void) { + if (!this.bounds.hasIntersectionWith(configSpaceBounds)) return for (let child of this.children) { - child.forEachBatchWithinBounds(configSpaceViewport, cb) + child.forEachLeafNodeWithinBounds(configSpaceBounds, cb) } } } @@ -68,149 +160,108 @@ export interface FlamechartRendererProps { physicalSpaceDstRect: Rect } -class BoundedLayer { - private rootNode: RangeTreeNode - constructor( - private canvasContext: CanvasContext, - flamechart: Flamechart, - private stackDepth: number - ) { - const leafNodes: RangeTreeLeafNode[] = [] - - let minLeft = Infinity - let maxRight = -Infinity - let batch = canvasContext.createRectangleBatch() - - for (let frame of flamechart.getLayers()[stackDepth]) { - if (batch.getRectCount() >= MAX_BATCH_SIZE) { - leafNodes.push(new RangeTreeLeafNode(batch, minLeft, maxRight)) - minLeft = Infinity - maxRight = -Infinity - batch = canvasContext.createRectangleBatch() - } - const configSpaceBounds = new Rect( - new Vec2(frame.start, stackDepth + 1), - new Vec2(frame.end - frame.start, 1) - ) - minLeft = Math.min(minLeft, configSpaceBounds.left()) - maxRight = Math.max(maxRight, configSpaceBounds.right()) - const color = flamechart.getColorForFrame(frame.node.frame) - batch.addRect(configSpaceBounds, color) - } - - if (batch.getRectCount() > 0) { - leafNodes.push(new RangeTreeLeafNode(batch, minLeft, maxRight)) - } - - // TODO(jlfwong): Probably want this to be a binary tree - this.rootNode = new RangeTreeInteriorNode(leafNodes) - } - - render(props: FlamechartRendererProps) { - const configSpaceTop = this.stackDepth + 1 - const configSpaceBottom = configSpaceTop + 1 - - const { configSpaceSrcRect, physicalSpaceDstRect } = props - if (configSpaceTop > configSpaceSrcRect.bottom()) { - // Entire layer is below the config space bounds - return - } - - if (configSpaceBottom < configSpaceSrcRect.top()) { - // Entire layer is above the config space bounds - return - } - - this.rootNode.forEachBatchWithinBounds(configSpaceSrcRect, batch => { - this.canvasContext.drawRectangleBatch({ - configSpaceSrcRect, - physicalSpaceDstRect, - batch - }) - }) - } -} export class FlamechartRenderer { - private layers: BoundedLayer[] = [] - private texture: regl.Texture | null = null + private root: RangeTreeNode + private rowAtlas: RowAtlas - private getConfigSpaceSize() { - return new Vec2(this.flamechart.getTotalWeight(), this.flamechart.getLayers().length) - } - - private getConfigSpaceContentRect() { - return new Rect(new Vec2(), this.getConfigSpaceSize()) - } - - constructor(private canvasContext: CanvasContext, private flamechart: Flamechart) { + constructor(private canvasContext: CanvasContext, flamechart: Flamechart) { const nLayers = flamechart.getLayers().length - const maxTextureSize = canvasContext.getMaxTextureSize() + this.rowAtlas = new RowAtlas(canvasContext) - if (nLayers > maxTextureSize) { - throw new Error(`This profile has more than ${maxTextureSize} layers!`) - } + const layers: RangeTreeNode[] = [] - for (let i = 0; i < nLayers; i++) { - this.layers.push(new BoundedLayer(canvasContext, flamechart, i)) - } + for (let stackDepth = 0; stackDepth < nLayers; stackDepth++) { + const leafNodes: RangeTreeLeafNode[] = [] + const y = stackDepth + 1 - this.texture = canvasContext.gl.texture({ - width: canvasContext.getMaxTextureSize(), - height: nLayers, - wrapS: 'clamp', - wrapT: 'clamp', - }) - const fbo = canvasContext.gl.framebuffer({ color: [this.texture] }) + let minLeft = Infinity + let maxRight = -Infinity + let batch = canvasContext.createRectangleBatch() - const configSpaceSrcRect = this.getConfigSpaceContentRect() - - canvasContext.gl({ - viewport: (context, props) => { - return { - x: 0, - y: 0, - width: canvasContext.getMaxTextureSize(), - height: nLayers + for (let frame of flamechart.getLayers()[stackDepth]) { + if (batch.getRectCount() >= MAX_BATCH_SIZE) { + leafNodes.push(new RangeTreeLeafNode(batch, new Rect( + new Vec2(minLeft, stackDepth), + new Vec2(maxRight - minLeft, 1) + ))) + minLeft = Infinity + maxRight = -Infinity + batch = canvasContext.createRectangleBatch() } - }, - framebuffer: fbo - })((context: regl.Context) => { - const physicalSpaceDstRect = new Rect( - new Vec2(), - new Vec2(context.viewportWidth, context.viewportHeight) - ) - for (let layer of this.layers) { - layer.render({ configSpaceSrcRect, physicalSpaceDstRect }) + const configSpaceBounds = new Rect( + new Vec2(frame.start, y), + new Vec2(frame.end - frame.start, 1) + ) + minLeft = Math.min(minLeft, configSpaceBounds.left()) + maxRight = Math.max(maxRight, configSpaceBounds.right()) + const color = flamechart.getColorForFrame(frame.node.frame) + batch.addRect(configSpaceBounds, color) } - }) - fbo.destroy() + if (batch.getRectCount() > 0) { + leafNodes.push(new RangeTreeLeafNode(batch, new Rect( + new Vec2(minLeft, stackDepth), + new Vec2(maxRight - minLeft, 1) + ))) + } + + // TODO(jlfwong): Probably want this to be a binary tree + layers.push(new RangeTreeInteriorNode(leafNodes)) + } + this.root = new RangeTreeInteriorNode(layers) } render(props: FlamechartRendererProps) { - if (!this.texture) return - const { configSpaceSrcRect, physicalSpaceDstRect } = props - const content = this.getConfigSpaceContentRect() - const textureRect = new Rect( - new Vec2(), new Vec2(this.texture.width, this.texture.height) - ) + let renderedBatchCount = 0 + let cacheCapacity = this.rowAtlas.getCapacity() - const configToTexture = AffineTransform.betweenRects(content, textureRect) - const physicalSpaceSrcRect = configToTexture.transformRect(configSpaceSrcRect) + const cachedLeaves: RangeTreeLeafNode[] = [] + const uncachedLeaves: RangeTreeLeafNode[] = [] - this.canvasContext.drawTexture({ - texture: this.texture, - srcRect: physicalSpaceSrcRect, - dstRect: physicalSpaceDstRect + this.root.forEachLeafNodeWithinBounds(configSpaceSrcRect, leaf => { + // We want to avoid rendering more batches to the cache than + // the capacity fo the cache to prevent LRU cache thrash. Imagine + // the capacity is 2 and you render 4 items via the cache. Every time + // you do this, you end up evicting and populating the cache on all 4 items, + // which is even more expensive than not using a cache at all! Instead, + // we'll cache the first 2 entries in that case, and re-use that cache each time, + // while rendering the final 2 items without use of the cache. + // An exception here is if the node is already in the cache! + let useCache = renderedBatchCount++ < cacheCapacity || this.rowAtlas.has(leaf) + + if (useCache) { + cachedLeaves.push(leaf) + } else { + uncachedLeaves.push(leaf) + } }) - /* - for (let layer of this.layers) { - layer.render(props) + this.rowAtlas.writeToAtlasIfNeeded(cachedLeaves, (textureDstRect, leaf) => { + this.canvasContext.drawRectangleBatch({ + batch: leaf.getBatch(), + configSpaceSrcRect: leaf.getBounds(), + physicalSpaceDstRect: textureDstRect + }) + }) + + const configToPhysical = AffineTransform.betweenRects(configSpaceSrcRect, physicalSpaceDstRect) + for (let leaf of cachedLeaves) { + const configSpaceLeafBounds = leaf.getBounds() + const physicalLeafBounds = configToPhysical.transformRect(configSpaceLeafBounds) + if (!this.rowAtlas.renderViaAtlas(leaf, physicalLeafBounds)) { + console.error('Failed to render from cache') + } + } + + for (let leaf of uncachedLeaves) { + this.canvasContext.drawRectangleBatch({ + batch: leaf.getBatch(), + configSpaceSrcRect, + physicalSpaceDstRect + }) } - */ } } \ No newline at end of file diff --git a/flamechart-view.tsx b/flamechart-view.tsx index 2d4bbc0..2517da8 100644 --- a/flamechart-view.tsx +++ b/flamechart-view.tsx @@ -310,7 +310,7 @@ export class FlamechartPanZoomView extends ReloadableComponent { this.props.flamechartRenderer.render({ - physicalSpaceDstRect: new Rect(new Vec2(), this.physicalViewSize()), + physicalSpaceDstRect: new Rect(Vec2.zero, this.physicalViewSize()), configSpaceSrcRect: this.props.configSpaceViewportRect }) }) @@ -463,7 +463,7 @@ export class FlamechartPanZoomView extends ReloadableComponent { this.hoveredLabel = null - this.props.setNodeHover(null, new Vec2()) + this.props.setNodeHover(null, Vec2.zero) this.renderCanvas() } @@ -573,8 +573,8 @@ export class FlamechartView extends ReloadableComponent { + prev: ListNode | null = null + next: ListNode | null = null + constructor(readonly data: V) { } +} + +export class List { + private head: ListNode | null = null + private tail: ListNode | null = null + private size: number = 0 + constructor() { } + + getHead(): ListNode | null { return this.head } + getTail(): ListNode | null { return this.tail } + getSize(): number { return this.size } + + append(node: ListNode): void { + if (!this.tail) { + this.head = this.tail = node + } else { + this.tail.next = node + node.prev = this.tail + this.tail = node + } + this.size++ + } + + prepend(node: ListNode): ListNode { + if (!this.head) { + this.head = this.tail = node + } else { + this.head.prev = node + node.next = this.head + this.head = node + } + this.size++ + return node + } + + pop(): ListNode | null { + if (!this.tail) { + return null + } else { + const ret = this.tail + if (ret.prev) { + this.tail = ret.prev + this.tail.next = null + } else { + this.head = this.tail = null + } + this.size-- + ret.prev = null + return ret + } + } + + dequeue(): ListNode | null { + if (!this.head) { + return null + } else { + const ret = this.head + if (ret.next) { + this.head = ret.next + this.head.prev = null + } else { + this.head = this.tail = null + } + this.size-- + ret.next = null + return ret + } + } + + remove(node: ListNode): void { + if (node.prev == null) { + this.dequeue() + } else if (node.next == null) { + this.pop() + } else { + // Neither first nor last, should be safe to just link + // neighbours. + node.next.prev = node.prev + node.prev.next = node.next + node.next = null + node.prev = null + } + this.size-- + } +} + +interface LRUCacheNode { + value: V + listNode: ListNode +} + +export class LRUCache { + private list = new List() + private map = new Map>() + + constructor(private capacity: number) { } + + has(key: K): boolean { + return this.map.has(key) + } + + get(key: K): V | null { + const node = this.map.get(key) + if (!node) { + return null + } + // Bring node to the front of the list + this.list.remove(node.listNode) + this.list.prepend(node.listNode) + + return node ? node.value : null + } + + getSize() { return this.list.getSize() } + + getCapacity() { return this.capacity } + + insert(key: K, value: V) { + const node = this.map.get(key) + if (node) { + this.list.remove(node.listNode) + } + // Evict old entries when out of capacity + while (this.list.getSize() >= this.capacity) { + this.map.delete(this.list.pop()!.data) + } + const listNode = this.list.prepend(new ListNode(key)) + this.map.set(key, { value, listNode }) + } + + getOrInsert(key: K, f: (key: K) => V): V { + let value = this.get(key) + if (value == null) { + value = f(key) + this.insert(key, value) + } + return value + } + + removeLRU(): [K, V] | null { + const oldest = this.list.pop() + if (!oldest) return null + const key = oldest.data + const value = this.map.get(key)!.value + this.map.delete(key) + return [key, value] + } +} \ No newline at end of file diff --git a/math.ts b/math.ts index c728193..e174a14 100644 --- a/math.ts +++ b/math.ts @@ -5,7 +5,7 @@ export function clamp(x: number, minVal: number, maxVal: number) { } export class Vec2 { - constructor(readonly x = 0, readonly y = 0) {} + constructor(readonly x: number, readonly y: number) {} withX(x: number) { return new Vec2(x, this.y) } withY(y: number) { return new Vec2(this.x, y) } @@ -28,6 +28,9 @@ export class Vec2 { return new Vec2(Math.max(a.x, b.x), Math.max(a.y, b.y)) } + static zero = new Vec2(0, 0) + static unit = new Vec2(1, 1) + flatten(): [number, number] { return [this.x, this.y] } } @@ -204,8 +207,8 @@ export class AffineTransform { export class Rect { constructor( - readonly origin = new Vec2(), - readonly size = new Vec2() + readonly origin: Vec2, + readonly size: Vec2 ) {} isEmpty() { return this.width() == 0 || this.height() == 0 } @@ -263,4 +266,8 @@ export class Rect { equals(other: Rect) { return this.origin.equals(other.origin) && this.size.equals(other.size) } + + static empty = new Rect(Vec2.zero, Vec2.zero) + static unit = new Rect(Vec2.zero, Vec2.unit) + static NDC = new Rect(new Vec2(-1, -1), new Vec2(2, 2)) } \ No newline at end of file diff --git a/package.json b/package.json index f73bbb3..80fbd8e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "serve": "parcel -o dev.html", + "serve": "parcel --no-hmr -o dev.html", "release": "tsc --noEmit && parcel build speedscope.tsx" }, "author": "", diff --git a/texture-catched-renderer.ts b/texture-catched-renderer.ts index 15740c9..9e6b632 100644 --- a/texture-catched-renderer.ts +++ b/texture-catched-renderer.ts @@ -1,5 +1,5 @@ import * as regl from 'regl' -import { Vec2, Rect } from './math' +import { Vec2, Rect, AffineTransform } from './math' export class TextureRendererProps { texture: regl.Texture @@ -12,13 +12,16 @@ export class TextureRenderer { constructor(gl: regl.Instance) { this.command = gl({ vert: ` + uniform mat3 uvTransform; + uniform mat3 positionTransform; + attribute vec2 position; attribute vec2 uv; varying vec2 vUv; void main() { - vUv = uv; - gl_Position = vec4(position, 0, 1); + vUv = (uvTransform * vec3(uv, 1)).xy; + gl_Position = vec4((positionTransform * vec3(position, 1)).xy, 0, 1); } `, frag: ` @@ -45,48 +48,49 @@ export class TextureRenderer { // | /| // |/ | // 2 +--+ 3 - position: (context, props) => { - const { dstRect } = props - - const width = context.viewportWidth - const height = context.viewportHeight - - const left = 2 * (dstRect.left() / width) - 1 - const right = 2 * (dstRect.right() / width) - 1 - - const top = -(2 * (dstRect.top() / height) - 1) - const bottom = -(2 * (dstRect.bottom() / height) - 1) - - return [ - [left, top], - [right, top], - [left, bottom], - [right, bottom] - ] - }, - uv: (context, props) => { - const { srcRect } = props - - const width = props.texture.width - const height = props.texture.height - - const left = srcRect.left() / width - const right = srcRect.right() / width - - const top = 1 - srcRect.top() / height - const bottom = 1 - srcRect.bottom() / height - - return [ - [left, top], - [right, top], - [left, bottom], - [right, bottom] - ] - } + position: gl.buffer([ + [-1, 1], + [1, 1], + [-1, -1], + [1, -1] + ]), + uv: gl.buffer([ + [0, 1], + [1, 1], + [0, 0], + [1, 0] + ]) }, uniforms: { - texture: (context, props) => props.texture + texture: (context, props) => props.texture, + uvTransform: (context, props) => { + const { srcRect, texture } = props + const physicalToUV = AffineTransform.withTranslation(new Vec2(0, 1)) + .times(AffineTransform.withScale(new Vec2(1, -1))) + .times(AffineTransform.betweenRects( + new Rect(Vec2.zero, new Vec2(texture.width, texture.height)), + Rect.unit + )) + const uvRect = physicalToUV.transformRect(srcRect) + return AffineTransform.betweenRects( + Rect.unit, + uvRect, + ).flatten() + }, + positionTransform: (context, props) => { + const { dstRect } = props + + const viewportSize = new Vec2(context.viewportWidth, context.viewportHeight) + + const physicalToNDC = AffineTransform.withScale(new Vec2(1, -1)) + .times(AffineTransform.betweenRects( + new Rect(Vec2.zero, viewportSize), + Rect.NDC) + ) + const ndcRect = physicalToNDC.transformRect(dstRect) + return AffineTransform.betweenRects(Rect.NDC, ndcRect).flatten() + } }, primitive: 'triangle strip', @@ -95,11 +99,10 @@ export class TextureRenderer { }) } - render(context: regl.Context, props: TextureRendererProps) { + render(props: TextureRendererProps) { this.command(props) } - resetStats() { return Object.assign(this.command.stats, { cpuTime: 0, gpuTime: 0, count: 0 }) } stats() { return this.command.stats } } @@ -116,6 +119,7 @@ export class TextureCachedRenderer { private texture: regl.Texture private framebuffer: regl.Framebuffer private textureRenderer: TextureRenderer + private withContext: regl.Command<{}> constructor(private gl: regl.Instance, options: TextureCachedRendererOptions) { this.renderUncached = options.render @@ -124,6 +128,7 @@ export class TextureCachedRenderer { this.texture = gl.texture(1, 1) this.framebuffer = gl.framebuffer({color: [this.texture]}) + this.withContext = gl({}) } private lastRenderProps: T | null = null @@ -133,47 +138,49 @@ export class TextureCachedRenderer { this.dirty = true } - render(context: regl.Context, props: T) { - let needsRender = false - if (this.texture.width !== context.viewportWidth || this.texture.height !== context.viewportHeight) { - this.texture({ width: context.viewportWidth, height: context.viewportHeight }) - this.framebuffer({ color: [this.texture] }) - needsRender = true - } else if (this.lastRenderProps == null) { - needsRender = true - } else if (this.shouldUpdate(this.lastRenderProps, props)) { - needsRender = true - } else if (this.dirty) { - needsRender = true - } + render(props: T) { + this.withContext((context: regl.Context) => { + let needsRender = false + if (this.texture.width !== context.viewportWidth || this.texture.height !== context.viewportHeight) { + this.texture({ width: context.viewportWidth, height: context.viewportHeight }) + this.framebuffer({ color: [this.texture] }) + needsRender = true + } else if (this.lastRenderProps == null) { + needsRender = true + } else if (this.shouldUpdate(this.lastRenderProps, props)) { + needsRender = true + } else if (this.dirty) { + needsRender = true + } - if (needsRender) { - // Render to texture - this.gl({ - viewport: (context, props) => { - return { - x: 0, - y: 0, - width: context.viewportWidth, - height: context.viewportHeight - } - }, - framebuffer: this.framebuffer - })(() => { - this.gl.clear({color: [0, 0, 0, 0]}) - this.renderUncached(props) + if (needsRender) { + // Render to texture + this.gl({ + viewport: (context, props) => { + return { + x: 0, + y: 0, + width: context.viewportWidth, + height: context.viewportHeight + } + }, + framebuffer: this.framebuffer + })(() => { + this.gl.clear({color: [0, 0, 0, 0]}) + this.renderUncached(props) + }) + } + + const glViewportRect = new Rect(Vec2.zero, new Vec2(context.viewportWidth, context.viewportHeight)) + + // Render from texture + this.textureRenderer.render({ + texture: this.texture, + srcRect: glViewportRect, + dstRect: glViewportRect }) - } - - const glViewportRect = new Rect(new Vec2(), new Vec2(context.viewportWidth, context.viewportHeight)) - - // Render from texture - this.textureRenderer.render(context, { - texture: this.texture, - srcRect: glViewportRect, - dstRect: glViewportRect + this.lastRenderProps = props + this.dirty = false }) - this.lastRenderProps = props - this.dirty = false } } \ No newline at end of file