diff --git a/canvas-context.ts b/canvas-context.ts index 0cb93f3..21ed50b 100644 --- a/canvas-context.ts +++ b/canvas-context.ts @@ -5,6 +5,7 @@ import { TextureCachedRenderer, TextureRenderer, TextureRendererProps } from './ import { StatsPanel } from './stats' import { Vec2, Rect } from './math'; +import { OutlineRenderer, OutlineRendererProps } from './outline-renderer'; type FrameCallback = () => void @@ -17,6 +18,7 @@ export class CanvasContext { private rectangleBatchRenderer: RectangleBatchRenderer private viewportRectangleRenderer: ViewportRectangleRenderer private textureRenderer: TextureRenderer + private outlineRenderer: OutlineRenderer private setViewportScope: regl.Command<{ physicalBounds: Rect }> private setScissor: regl.Command<{}> @@ -26,7 +28,7 @@ export class CanvasContext { attributes: { antialias: false }, - extensions: ['ANGLE_instanced_arrays'], + extensions: ['ANGLE_instanced_arrays', 'WEBGL_depth_texture'], optionalExtensions: ['EXT_disjoint_timer_query'], profile: true }) @@ -34,6 +36,7 @@ export class CanvasContext { this.rectangleBatchRenderer = new RectangleBatchRenderer(this.gl) this.viewportRectangleRenderer = new ViewportRectangleRenderer(this.gl) this.textureRenderer = new TextureRenderer(this.gl) + this.outlineRenderer = new OutlineRenderer(this.gl) this.setScissor = this.gl({ scissor: { enable: true } }) this.setViewportScope = this.gl({ context: { @@ -122,6 +125,10 @@ export class CanvasContext { this.textureRenderer.render(props) } + drawOutlines(props: OutlineRendererProps) { + this.outlineRenderer.render(props) + } + createRectangleBatch(): RectangleBatch { return new RectangleBatch(this.gl) } diff --git a/flamechart-renderer.ts b/flamechart-renderer.ts index 95f9ed4..fed4320 100644 --- a/flamechart-renderer.ts +++ b/flamechart-renderer.ts @@ -112,7 +112,8 @@ class RangeTreeLeafNode implements RangeTreeNode { constructor( private batch: RectangleBatch, - private bounds: Rect + private bounds: Rect, + private numPrecedingRectanglesInRow: number ) { batch.uploadToGPU() } @@ -121,6 +122,7 @@ class RangeTreeLeafNode implements RangeTreeNode { getBounds() { return this.bounds } getRectCount() { return this.batch.getRectCount() } getChildren() { return this.children } + getParity() { return this.numPrecedingRectanglesInRow % 2 } forEachLeafNodeWithinBounds(configSpaceBounds: Rect, cb: (leaf: RangeTreeLeafNode) => void) { if (!this.bounds.hasIntersectionWith(configSpaceBounds)) return cb(this) @@ -178,11 +180,15 @@ interface FlamechartRowAtlasKey { export class FlamechartRenderer { private layers: RangeTreeNode[] = [] private rowAtlas: RowAtlas + private colorTexture: regl.Texture + private depthTexture: regl.Texture + private framebuffer: regl.Framebuffer + private renderToFramebuffer: regl.Command<{}> + private withContext: regl.Command<{}> constructor(private canvasContext: CanvasContext, private flamechart: Flamechart) { const nLayers = flamechart.getLayers().length this.rowAtlas = new RowAtlas(canvasContext) - for (let stackDepth = 0; stackDepth < nLayers; stackDepth++) { const leafNodes: RangeTreeLeafNode[] = [] const y = stackDepth @@ -191,12 +197,14 @@ export class FlamechartRenderer { let maxRight = -Infinity let batch = canvasContext.createRectangleBatch() + let rectCount = 0 + 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) - ))) + ), rectCount)) minLeft = Infinity maxRight = -Infinity batch = canvasContext.createRectangleBatch() @@ -209,17 +217,33 @@ export class FlamechartRenderer { maxRight = Math.max(maxRight, configSpaceBounds.right()) const color = flamechart.getColorForFrame(frame.node.frame) batch.addRect(configSpaceBounds, color) + rectCount++ } if (batch.getRectCount() > 0) { leafNodes.push(new RangeTreeLeafNode(batch, new Rect( new Vec2(minLeft, stackDepth), new Vec2(maxRight - minLeft, 1) - ))) + ), rectCount)) } - // TODO(jlfwong): Probably want this to be a binary tree + // TODO(jlfwong): Making this into a binary tree + // range than a tree of always-height-two might make this run faster this.layers.push(new RangeTreeInteriorNode(leafNodes)) + + // TODO(jlfwong): Extract this to CanvasContext + this.withContext = canvasContext.gl({}) + + this.colorTexture = this.canvasContext.gl.texture({ width: 1, height: 1 }) + this.depthTexture = this.canvasContext.gl.texture({ width: 1, height: 1, format: 'depth', type: 'uint16' }) + this.framebuffer = this.canvasContext.gl.framebuffer({ + color: [this.colorTexture], + depth: this.depthTexture + }) + + this.renderToFramebuffer = canvasContext.gl({ + framebuffer: this.framebuffer + }) } } @@ -261,7 +285,7 @@ export class FlamechartRenderer { } const top = Math.max(0, Math.floor(configSpaceSrcRect.top())) - const bottom = Math.min(this.layers.length - 1, Math.ceil(configSpaceSrcRect.bottom())) + const bottom = Math.min(this.layers.length, Math.ceil(configSpaceSrcRect.bottom())) const configSpaceContentWidth = this.flamechart.getTotalWeight() const numAtlasEntriesPerLayer = Math.pow(2, zoomLevel) @@ -288,29 +312,61 @@ export class FlamechartRenderer { this.canvasContext.drawRectangleBatch({ batch: leaf.getBatch(), configSpaceSrcRect: configSpaceBounds, - physicalSpaceDstRect: textureDstRect + physicalSpaceDstRect: textureDstRect, + parityMin: key.stackDepth % 2 == 0 ? 2 : 0, + parityOffset: leaf.getParity() }) }) }) - // Render from the cache - for (let key of keysToRenderCached) { - const configSpaceSrcRect = this.configSpaceBoundsForKey(key) - this.rowAtlas.renderViaAtlas(key, configToPhysical.transformRect(configSpaceSrcRect)) - } + this.withContext((context: regl.Context) => { + this.framebuffer.resize(context.viewportWidth, context.viewportHeight) + }) - // Render entries that didn't make it into the cache - for (let key of keysToRenderUncached) { - const configSpaceBounds = this.configSpaceBoundsForKey(key) - const physicalBounds = configToPhysical.transformRect(configSpaceBounds) - this.layers[key.stackDepth].forEachLeafNodeWithinBounds(configSpaceBounds, (leaf) => { - this.canvasContext.drawRectangleBatch({ - batch: leaf.getBatch(), - configSpaceSrcRect, - physicalSpaceDstRect: physicalBounds + this.renderToFramebuffer((context: regl.Context) => { + // this.withContext((context: regl.Context) => { + this.canvasContext.gl.clear({color: [0, 0, 0, 0]}) + const viewportRect = new Rect(Vec2.zero, new Vec2(context.viewportWidth, context.viewportHeight)) + + const configToViewport = AffineTransform.betweenRects(configSpaceSrcRect, viewportRect) + + // Render from the cache + for (let key of keysToRenderCached) { + const configSpaceSrcRect = this.configSpaceBoundsForKey(key) + this.rowAtlas.renderViaAtlas(key, configToViewport.transformRect(configSpaceSrcRect)) + } + + // Render entries that didn't make it into the cache + for (let key of keysToRenderUncached) { + const configSpaceBounds = this.configSpaceBoundsForKey(key) + const physicalBounds = configToViewport.transformRect(configSpaceBounds) + this.layers[key.stackDepth].forEachLeafNodeWithinBounds(configSpaceBounds, (leaf) => { + this.canvasContext.drawRectangleBatch({ + batch: leaf.getBatch(), + configSpaceSrcRect, + physicalSpaceDstRect: physicalBounds, + parityMin: key.stackDepth % 2 == 0 ? 2 : 0, + parityOffset: leaf.getParity() + }) }) - }) - } + } + }) + + this.canvasContext.drawOutlines({ + colorTexture: this.colorTexture, + depthTexture: this.depthTexture, + srcRect: new Rect(Vec2.zero, new Vec2(this.colorTexture.width, this.colorTexture.height)), + dstRect: physicalSpaceDstRect + }) + + // Paint the color texture for debugging + /* + this.canvasContext.drawTexture({ + texture: this.colorTexture, + srcRect: new Rect(Vec2.zero, new Vec2(this.colorTexture.width, this.colorTexture.height)), + dstRect: physicalSpaceDstRect + }) + */ // Overlay the atlas on top of the canvas for debugging /* diff --git a/outline-renderer.ts b/outline-renderer.ts new file mode 100644 index 0000000..d352b20 --- /dev/null +++ b/outline-renderer.ts @@ -0,0 +1,130 @@ +import * as regl from 'regl' +import { Vec2, Rect, AffineTransform } from './math' + +export class OutlineRendererProps { + colorTexture: regl.Texture + depthTexture: regl.Texture + srcRect: Rect + dstRect: Rect +} +export class OutlineRenderer { + private command: regl.Command + 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 = (uvTransform * vec3(uv, 1)).xy; + gl_Position = vec4((positionTransform * vec3(position, 1)).xy, 0, 1); + } + `, + + frag: ` + precision mediump float; + + uniform vec2 uvSpacePixelSize; + + varying vec2 vUv; + uniform sampler2D colorTexture; + uniform sampler2D depthTexture; + + void main() { + // Sample the 4 surrounding pixels in the depth texture to determine + // if we should draw a boundary here or not. + float N = texture2D(depthTexture, vUv + vec2(0, uvSpacePixelSize.y)).r; + float E = texture2D(depthTexture, vUv + vec2(uvSpacePixelSize.x, 0)).r; + float S = texture2D(depthTexture, vUv + vec2(0, -uvSpacePixelSize.y)).r; + float W = texture2D(depthTexture, vUv + vec2(-uvSpacePixelSize.x, 0)).r; + float here = texture2D(depthTexture, vUv).x; + + if ( + here == N && here != S || // Top edge + here == S && here != N || // Bottom edge + here == E && here != W || // Left edge + here == W && here != E + ) { + // We're on an edge! Draw white. + gl_FragColor = vec4(1, 1, 1, 1); + } else { + gl_FragColor = texture2D(colorTexture, vUv); + } + } + `, + + depth: { + enable: false + }, + + attributes: { + // Cover full canvas with a rectangle + // with 2 triangles using a triangle + // strip. + // + // 0 +--+ 1 + // | /| + // |/ | + // 2 +--+ 3 + position: gl.buffer([ + [-1, 1], + [1, 1], + [-1, -1], + [1, -1] + ]), + uv: gl.buffer([ + [0, 1], + [1, 1], + [0, 0], + [1, 0] + ]) + }, + + count: 4, + + primitive: 'triangle strip', + + uniforms: { + colorTexture: (context, props) => props.colorTexture, + depthTexture: (context, props) => props.depthTexture, + uvTransform: (context, props) => { + const { srcRect, colorTexture } = 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(colorTexture.width, colorTexture.height)), + Rect.unit + )) + const uvRect = physicalToUV.transformRect(srcRect) + return AffineTransform.betweenRects( + Rect.unit, + uvRect, + ).flatten() + }, + uvSpacePixelSize: (context, props) => { + return Vec2.unit.dividedByPointwise(new Vec2(props.colorTexture.width, props.colorTexture.height)).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() + } + }, + }) + } + + render(props: OutlineRendererProps) { + this.command(props) + } +} diff --git a/rectangle-batch-renderer.ts b/rectangle-batch-renderer.ts index 4608f3f..6e0321b 100644 --- a/rectangle-batch-renderer.ts +++ b/rectangle-batch-renderer.ts @@ -32,10 +32,21 @@ export class RectangleBatch { return this.colorBuffer } + private indexBuffer: regl.Buffer | null = null + getIndexBuffer() { + if (!this.indexBuffer) { + const indices = new Float32Array(this.rectCount) + for (let i = 0; i < this.rectCount; i++) indices[i] = i + this.indexBuffer = this.gl.buffer(indices) + } + return this.indexBuffer + } + uploadToGPU() { this.getConfigSpaceOffsetBuffer() this.getConfigSpaceSizeBuffer() this.getColorBuffer() + this.getIndexBuffer() } addRect(rect: Rect, color: Color) { @@ -71,14 +82,23 @@ export interface RectangleBatchRendererProps { batch: RectangleBatch configSpaceSrcRect: Rect physicalSpaceDstRect: Rect + parityMin?: number + parityOffset?: number } export class RectangleBatchRenderer { private command: regl.Command constructor(gl: regl.Instance) { + // We draw the parity / 4 into the depth channel so it + // can be used in a post-processing step to draw boundaries + // between rectangles. We use 4 different values (2 per row) + // so we can distingish both between adjacent rectangles on a row + // and between rows! this.command = gl({ vert: ` uniform mat3 configSpaceToNDC; + uniform float parityMin; + uniform float parityOffset; // Non-instanced attribute vec2 corner; @@ -87,23 +107,27 @@ export class RectangleBatchRenderer { attribute vec2 configSpaceOffset; attribute vec2 configSpaceSize; attribute vec3 color; + attribute float index; + varying vec3 vColor; void main() { vColor = color; + float depth = parityMin + mod(parityOffset + index, 2.0); vec2 configSpacePos = configSpaceOffset + corner * configSpaceSize; vec2 position = (configSpaceToNDC * vec3(configSpacePos, 1)).xy; - gl_Position = vec4(position, 0, 1); + gl_Position = vec4(position, depth / 4.0, 1); } `, - depth: { - enable: false - }, + depth: { + enable: false + }, - frag: ` + frag: ` precision mediump float; varying vec3 vColor; + varying float vParity; void main() { gl_FragColor = vec4(vColor, 1); } @@ -145,6 +169,15 @@ export class RectangleBatchRenderer { size: 3, divisor: 1 } + }, + index: (context, props) => { + return { + buffer: props.batch.getIndexBuffer(), + offset: 0, + stride: 4, + size: 1, + divisor: 1 + } } }, @@ -161,6 +194,14 @@ export class RectangleBatchRenderer { .times(AffineTransform.withScale(new Vec2(2, -2).dividedByPointwise(viewportSize))) return physicalToNDC.times(configToPhysical).flatten() + }, + + parityOffset: (context, props) => { + return props.parityOffset || 0 + }, + + parityMin: (context, props) => { + return props.parityMin || 0 } }, diff --git a/regl.d.ts b/regl.d.ts index 83072c4..4866152 100644 --- a/regl.d.ts +++ b/regl.d.ts @@ -222,6 +222,7 @@ declare module "regl" { height: number (args: TextureArgs): void + resize(width: number, height: number): void destroy(): void stats: { size: number @@ -259,6 +260,11 @@ declare module "regl" { colorType?: 'uint8' | 'half float' | 'float' } interface Framebuffer { + width: number + height: number + color?: RenderBuffer[] + depth?: RenderBuffer + (options: FramebufferOptions): void resize(width: number, height: number): void destroy(): void @@ -414,14 +420,9 @@ declare module "regl" { (p: P, cb: (context: Context) => void): void stats: { - /** Aggregate time spent on the GPU in ms */ gpuTime: number - - /** Aggregate time spent on the CPU in ms */ cpuTime: number - - /** Total number of calls of this command */ - count: number + hitCount: number } } } diff --git a/sample/alternating.txt b/sample/alternating.txt new file mode 100644 index 0000000..53edf0e --- /dev/null +++ b/sample/alternating.txt @@ -0,0 +1,182 @@ +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 +a;b 1 +c;d 1 \ No newline at end of file diff --git a/texture-catched-renderer.ts b/texture-catched-renderer.ts index 9e6b632..30e25f3 100644 --- a/texture-catched-renderer.ts +++ b/texture-catched-renderer.ts @@ -142,6 +142,7 @@ export class TextureCachedRenderer { this.withContext((context: regl.Context) => { let needsRender = false if (this.texture.width !== context.viewportWidth || this.texture.height !== context.viewportHeight) { + // TODO(jlfwong): Can probably just use this.framebuffer.resize this.texture({ width: context.viewportWidth, height: context.viewportHeight }) this.framebuffer({ color: [this.texture] }) needsRender = true @@ -155,6 +156,9 @@ export class TextureCachedRenderer { if (needsRender) { // Render to texture + // TODO(jlfwong): Re-enable this when I figure out how to + // resize a framebuffer while another framebuffer is active. + /* this.gl({ viewport: (context, props) => { return { @@ -169,6 +173,7 @@ export class TextureCachedRenderer { this.gl.clear({color: [0, 0, 0, 0]}) this.renderUncached(props) }) + */ } const glViewportRect = new Rect(Vec2.zero, new Vec2(context.viewportWidth, context.viewportHeight))