Row atlas sort of working?

This commit is contained in:
Jamie Wong
2018-01-28 03:13:12 -08:00
parent 5412573ba3
commit 4fcd40da98
9 changed files with 611 additions and 244 deletions
+4 -7
View File
@@ -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<SetViewportScopeProps>({
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 {
+153
View File
@@ -5,6 +5,159 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>speedscope</title>
<script>
// https://github.com/evanw/webgl-recorder
false && (function() {
var getContext = HTMLCanvasElement.prototype.getContext;
var requestAnimationFrame = window.requestAnimationFrame;
var frameSincePageLoad = 0;
function countFrames() {
frameSincePageLoad++;
requestAnimationFrame(countFrames);
}
window.requestAnimationFrame = function() {
return requestAnimationFrame.apply(window, arguments);
};
HTMLCanvasElement.prototype.getContext = function(type) {
var canvas = this;
var context = getContext.apply(canvas, arguments);
if (type === 'webgl' || type === 'experimental-webgl') {
var oldWidth = canvas.width;
var oldHeight = canvas.height;
var oldFrameCount = frameSincePageLoad;
var trace = [];
var variables = {};
var fakeContext = {
trace: trace,
compileTrace: compileTrace,
downloadTrace: downloadTrace,
};
trace.push(' gl.canvas.width = ' + oldWidth + ';');
trace.push(' gl.canvas.height = ' + oldHeight + ';');
function compileTrace() {
var text = 'function* render(gl) {\n';
text += ' // Recorded using https://github.com/evanw/webgl-recorder\n';
for (var key in variables) {
text += ' var ' + key + 's = [];\n';
}
text += trace.join('\n');
text += '\n}\n';
return text;
}
function downloadTrace() {
var text = compileTrace();
var link = document.createElement('a');
link.href = URL.createObjectURL(new Blob([text], {type: 'application/javascript'}));
link.download = 'trace.js';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function getVariable(value) {
if (value instanceof WebGLActiveInfo ||
value instanceof WebGLBuffer ||
value instanceof WebGLFramebuffer ||
value instanceof WebGLProgram ||
value instanceof WebGLRenderbuffer ||
value instanceof WebGLShader ||
value instanceof WebGLShaderPrecisionFormat ||
value instanceof WebGLTexture ||
value instanceof WebGLUniformLocation) {
var name = value.constructor.name;
var list = variables[name] || (variables[name] = []);
var index = list.indexOf(value);
if (index === -1) {
index = list.length;
list.push(value);
}
return name + 's[' + index + ']';
}
return null;
}
console.timeStamp('start')
var start = performance.now()
for (var key in context) {
var value = context[key];
if (typeof value === 'function') {
fakeContext[key] = function(key, value) {
return function() {
trace.push(`// ${performance.now() - start}`)
var result = value.apply(context, arguments);
var args = [];
if (frameSincePageLoad !== oldFrameCount) {
oldFrameCount = frameSincePageLoad;
trace.push(' yield;');
}
if (canvas.width !== oldWidth || canvas.height !== oldHeight) {
oldWidth = canvas.width;
oldHeight = canvas.height;
trace.push(' gl.canvas.width = ' + oldWidth + ';');
trace.push(' gl.canvas.height = ' + oldHeight + ';');
}
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
if (typeof arg === 'number' || typeof arg === 'boolean' || typeof arg === 'string' || arg === null) {
args.push(JSON.stringify(arg));
}
else if (ArrayBuffer.isView(arg)) {
args.push('new ' + arg.constructor.name + '([' + Array.prototype.slice.call(arg) + '])');
}
else {
var variable = getVariable(arg);
if (variable !== null) {
args.push(variable);
}
else {
console.log('unsupported value:', arg);
args.push('null');
}
}
}
var text = 'gl.' + key + '(' + args.join(', ') + ');';
var variable = getVariable(result);
if (variable !== null) text = variable + ' = ' + text;
trace.push(' ' + text);
return result;
};
}(key, value);
}
else {
fakeContext[key] = value;
}
}
return fakeContext;
}
return context;
};
countFrames();
})();
</script>
<link rel="stylesheet" href="reset.css">
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
+1 -1
View File
@@ -99,7 +99,7 @@ export class FlamechartMinimapView extends Component<FlamechartMinimapViewProps,
}
this.props.canvasContext.renderInto(this.container, (context) => {
this.cachedRenderer!.render(context, {
this.cachedRenderer!.render({
physicalSize: this.physicalViewSize()
})
this.props.canvasContext.drawViewportRectangle({
+196 -145
View File
@@ -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<K> {
private texture: regl.Texture
private framebuffer: regl.Framebuffer
private renderToFramebuffer: regl.Command<{}>
private rowCache: LRUCache<K, number>
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<RangeTreeLeafNode>
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
})
}
*/
}
}
+4 -4
View File
@@ -310,7 +310,7 @@ export class FlamechartPanZoomView extends ReloadableComponent<FlamechartPanZoom
this.props.canvasContext.renderInto(this.container, () => {
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<FlamechartPanZoom
private onMouseLeave = (ev: MouseEvent) => {
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<FlamechartViewProps, Fla
super()
this.state = {
hoveredNode: null,
configSpaceViewportRect: new Rect(),
logicalSpaceMouse: new Vec2()
configSpaceViewportRect: Rect.empty,
logicalSpaceMouse: Vec2.zero
}
}
+152
View File
@@ -0,0 +1,152 @@
class ListNode<V> {
prev: ListNode<V> | null = null
next: ListNode<V> | null = null
constructor(readonly data: V) { }
}
export class List<V> {
private head: ListNode<V> | null = null
private tail: ListNode<V> | null = null
private size: number = 0
constructor() { }
getHead(): ListNode<V> | null { return this.head }
getTail(): ListNode<V> | null { return this.tail }
getSize(): number { return this.size }
append(node: ListNode<V>): 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<V>): ListNode<V> {
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<V> | 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<V> | 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<V>): 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<K, V> {
value: V
listNode: ListNode<K>
}
export class LRUCache<K, V> {
private list = new List<K>()
private map = new Map<K, LRUCacheNode<K, V>>()
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]
}
}
+10 -3
View File
@@ -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))
}
+1 -1
View File
@@ -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": "",
+90 -83
View File
@@ -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<T> {
private texture: regl.Texture
private framebuffer: regl.Framebuffer
private textureRenderer: TextureRenderer
private withContext: regl.Command<{}>
constructor(private gl: regl.Instance, options: TextureCachedRendererOptions<T>) {
this.renderUncached = options.render
@@ -124,6 +128,7 @@ export class TextureCachedRenderer<T> {
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<T> {
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
}
}