diff --git a/application.tsx b/application.tsx index 3b16488..52e5a05 100644 --- a/application.tsx +++ b/application.tsx @@ -12,7 +12,6 @@ import {Profile, Frame} from './profile' import {Flamechart} from './flamechart' import { FlamechartView } from './flamechart-view' import { FontFamily, FontSize, Colors } from './style' -import { FrameColorGenerator } from './color' const enum SortOrder { CHRONO, @@ -224,13 +223,26 @@ export class Application extends ReloadableComponent<{}, ApplicationState> { const frames: Frame[] = [] profile.forEachFrame(f => frames.push(f)) - const colorGenerator = new FrameColorGenerator(frames) + function key(f: Frame) { + return (f.file || '') + f.name + } + function compare(a: Frame, b: Frame) { + return key(a) > key(b) ? 1 : -1 + } + frames.sort(compare) + const frameToColorBucket = new Map() + for (let i = 0; i < frames.length; i++) { + frameToColorBucket.set(frames[i], Math.floor(255 * i / frames.length)) + } + function getColorBucketForFrame(frame: Frame) { + return frameToColorBucket.get(frame) || 0 + } const flamechart = new Flamechart({ getTotalWeight: profile.getTotalWeight.bind(profile), forEachCall: profile.forEachCall.bind(profile), formatValue: profile.formatValue.bind(profile), - getColorForFrame: colorGenerator.getColorForFrame.bind(colorGenerator) + getColorBucketForFrame }) const flamechartRenderer = new FlamechartRenderer(this.canvasContext, flamechart) @@ -238,7 +250,7 @@ export class Application extends ReloadableComponent<{}, ApplicationState> { getTotalWeight: profile.getTotalNonIdleWeight.bind(profile), forEachCall: profile.forEachCallGrouped.bind(profile), formatValue: profile.formatValue.bind(profile), - getColorForFrame: colorGenerator.getColorForFrame.bind(colorGenerator) + getColorBucketForFrame }) const sortedFlamechartRenderer = new FlamechartRenderer(this.canvasContext, sortedFlamechart) diff --git a/canvas-context.ts b/canvas-context.ts index 21ed50b..2149bdd 100644 --- a/canvas-context.ts +++ b/canvas-context.ts @@ -5,7 +5,7 @@ import { TextureCachedRenderer, TextureRenderer, TextureRendererProps } from './ import { StatsPanel } from './stats' import { Vec2, Rect } from './math'; -import { OutlineRenderer, OutlineRendererProps } from './outline-renderer'; +import { FlamechartColorPassRenderer, FlamechartColorPassRenderProps } from './flamechart-color-pass-renderer'; type FrameCallback = () => void @@ -18,7 +18,7 @@ export class CanvasContext { private rectangleBatchRenderer: RectangleBatchRenderer private viewportRectangleRenderer: ViewportRectangleRenderer private textureRenderer: TextureRenderer - private outlineRenderer: OutlineRenderer + private flamechartColorPassRenderer: FlamechartColorPassRenderer private setViewportScope: regl.Command<{ physicalBounds: Rect }> private setScissor: regl.Command<{}> @@ -36,7 +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.flamechartColorPassRenderer = new FlamechartColorPassRenderer(this.gl) this.setScissor = this.gl({ scissor: { enable: true } }) this.setViewportScope = this.gl({ context: { @@ -125,8 +125,8 @@ export class CanvasContext { this.textureRenderer.render(props) } - drawOutlines(props: OutlineRendererProps) { - this.outlineRenderer.render(props) + drawFlamechartColorPass(props: FlamechartColorPassRenderProps) { + this.flamechartColorPassRenderer.render(props) } createRectangleBatch(): RectangleBatch { diff --git a/color.ts b/color.ts index 4f1ebec..1b558a0 100644 --- a/color.ts +++ b/color.ts @@ -27,6 +27,7 @@ function fract(x: number) { return x - Math.floor(x) } +// TODO(jlfwong): Can probably delete this? export class FrameColorGenerator { private frameToColor = new Map() diff --git a/flamechart-color-pass-renderer.ts b/flamechart-color-pass-renderer.ts new file mode 100644 index 0000000..e3686a4 --- /dev/null +++ b/flamechart-color-pass-renderer.ts @@ -0,0 +1,172 @@ +import * as regl from 'regl' +import { Vec2, Rect, AffineTransform } from './math' + +export class FlamechartColorPassRenderProps { + rectInfoTexture: regl.Texture + renderOutlines: boolean + srcRect: Rect + dstRect: Rect +} +export class FlamechartColorPassRenderer { + 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; + uniform float renderOutlines; + + varying vec2 vUv; + uniform sampler2D colorTexture; + + // https://en.wikipedia.org/wiki/HSL_and_HSV#From_luma/chroma/hue + vec3 lch2rgb(float L, float C, float H) { + float hPrime = H / 60.0; + float X = C * (1.0 - abs(mod(hPrime, 2.0) - 1.0)); + vec3 RGB = + hPrime < 1.0 ? vec3(C, X, 0) : + hPrime < 2.0 ? vec3(X, C, 0) : + hPrime < 3.0 ? vec3(0, C, X) : + hPrime < 4.0 ? vec3(0, X, C) : + hPrime < 5.0 ? vec3(X, 0, C) : + vec3(C, 0, X); + + float m = L - dot(RGB, vec3(0.30, 0.59, 0.11)); + return RGB + vec3(m, m, m); + } + + vec3 colorForBucket(float bucket) { + float x = 2.0 * fract(100.0 * bucket) - 1.0; + float L = 0.85 - 0.1 * x; + float C = 0.20 + 0.1 * x; + float H = 360.0 * bucket; + return lch2rgb(L, C, H); + } + + void main() { + vec4 here = texture2D(colorTexture, vUv); + + if (here.z == 0.0) { + // Background color + gl_FragColor = vec4(0, 0, 0, 0); + return; + } + + // Sample the 4 surrounding pixels in the depth texture to determine + // if we should draw a boundary here or not. + vec4 N = texture2D(colorTexture, vUv + vec2(0, uvSpacePixelSize.y)); + vec4 E = texture2D(colorTexture, vUv + vec2(uvSpacePixelSize.x, 0)); + vec4 S = texture2D(colorTexture, vUv + vec2(0, -uvSpacePixelSize.y)); + vec4 W = texture2D(colorTexture, vUv + vec2(-uvSpacePixelSize.x, 0)); + + // NOTE: For outline checks, we intentionally check both the right + // and the left to determine if we're an edge. If a rectangle is a single + // pixel wide, we don't want to render it as an outline, so this method + // of checking ensures that we don't outline single physical-space + // pixel width rectangles. + if ( + renderOutlines > 0.0 && + ( + here.y == N.y && here.y != S.y || // Top edge + here.y == S.y && here.y != N.y || // Bottom edge + here.x == E.x && here.x != W.x || // Left edge + here.x == W.x && here.x != E.x + ) + ) { + // We're on an edge! Draw white. + gl_FragColor = vec4(1, 1, 1, 1); + } else { + // Not on an edge. Draw the appropriate color; + gl_FragColor = vec4(colorForBucket(here.z), here.a); + } + } + `, + + 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.rectInfoTexture, + uvTransform: (context, props) => { + const { srcRect, rectInfoTexture } = 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(rectInfoTexture.width, rectInfoTexture.height)), + Rect.unit + )) + const uvRect = physicalToUV.transformRect(srcRect) + return AffineTransform.betweenRects( + Rect.unit, + uvRect, + ).flatten() + }, + renderOutlines: (context, props) => { + return props.renderOutlines ? 1.0 : 0.0 + }, + uvSpacePixelSize: (context, props) => { + return Vec2.unit.dividedByPointwise(new Vec2(props.rectInfoTexture.width, props.rectInfoTexture.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: FlamechartColorPassRenderProps) { + this.command(props) + } +} diff --git a/flamechart-minimap-view.tsx b/flamechart-minimap-view.tsx index d6f53a4..bddeb4a 100644 --- a/flamechart-minimap-view.tsx +++ b/flamechart-minimap-view.tsx @@ -78,6 +78,10 @@ export class FlamechartMinimapView extends Component | null = null private renderRects() { if (!this.container) return + + // Hasn't resized yet -- no point in rendering yet + if (this.physicalViewSize().x < 2) return + if (!this.cachedRenderer) { this.cachedRenderer = this.props.canvasContext.createTextureCachedRenderer({ shouldUpdate(oldProps, newProps) { @@ -93,15 +97,29 @@ export class FlamechartMinimapView extends Component { + // TODO(jlfwong): Switch back to the texture cached renderer once I figure out + // how to resize a framebuffer while another framebuffer is active. It seems + // to crash regl. I should submit a reduced repro case and hopefully get it fixed? + /* this.cachedRenderer!.render({ physicalSize: this.physicalViewSize() }) + */ + this.props.flamechartRenderer.render({ + configSpaceSrcRect: new Rect(new Vec2(0, 0), this.configSpaceSize()), + physicalSpaceDstRect: new Rect( + this.minimapOrigin(), + this.physicalViewSize().minus(this.minimapOrigin()) + ), + renderOutlines: false + }) this.props.canvasContext.drawViewportRectangle({ configSpaceViewportRect: this.props.configSpaceViewportRect, configSpaceToPhysicalViewSpace: this.configSpaceToPhysicalViewSpace(), diff --git a/flamechart-renderer.ts b/flamechart-renderer.ts index fed4320..2615816 100644 --- a/flamechart-renderer.ts +++ b/flamechart-renderer.ts @@ -29,7 +29,7 @@ class RowAtlas { framebuffer: this.framebuffer }) this.clearLineBatch = canvasContext.createRectangleBatch() - this.clearLineBatch.addRect(Rect.unit, new Color(1, 1, 1, 1)) + this.clearLineBatch.addRect(Rect.unit, new Color(0, 0, 0, 0)) } has(key: K) { return this.rowCache.has(key) } @@ -169,6 +169,7 @@ class RangeTreeInteriorNode implements RangeTreeNode { export interface FlamechartRendererProps { configSpaceSrcRect: Rect physicalSpaceDstRect: Rect + renderOutlines: boolean } interface FlamechartRowAtlasKey { @@ -180,8 +181,7 @@ interface FlamechartRowAtlasKey { export class FlamechartRenderer { private layers: RangeTreeNode[] = [] private rowAtlas: RowAtlas - private colorTexture: regl.Texture - private depthTexture: regl.Texture + private rectInfoTexture: regl.Texture private framebuffer: regl.Framebuffer private renderToFramebuffer: regl.Command<{}> private withContext: regl.Command<{}> @@ -199,7 +199,10 @@ export class FlamechartRenderer { let rectCount = 0 - for (let frame of flamechart.getLayers()[stackDepth]) { + const layer = flamechart.getLayers()[stackDepth] + + for (let i = 0; i < layer.length; i++) { + const frame = layer[i] if (batch.getRectCount() >= MAX_BATCH_SIZE) { leafNodes.push(new RangeTreeLeafNode(batch, new Rect( new Vec2(minLeft, stackDepth), @@ -215,7 +218,17 @@ export class FlamechartRenderer { ) minLeft = Math.min(minLeft, configSpaceBounds.left()) maxRight = Math.max(maxRight, configSpaceBounds.right()) - const color = flamechart.getColorForFrame(frame.node.frame) + + // We'll use the red channel to indicate the index to allow + // us to separate adjacent rectangles within a row from one another, + // the green channel to indicate the row, + // and the blue channel to indicate the color bucket to render. + // We add one to each so we have zero reserved for the background color. + const color = new Color( + (1 + i % 255) / 256, + (1 + stackDepth % 255) / 256, + (1 + this.flamechart.getColorBucketForFrame(frame.node.frame)) / 256 + ) batch.addRect(configSpaceBounds, color) rectCount++ } @@ -234,11 +247,9 @@ export class FlamechartRenderer { // 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.rectInfoTexture = this.canvasContext.gl.texture({ width: 1, height: 1 }) this.framebuffer = this.canvasContext.gl.framebuffer({ - color: [this.colorTexture], - depth: this.depthTexture + color: [this.rectInfoTexture], }) this.renderToFramebuffer = canvasContext.gl({ @@ -352,22 +363,13 @@ export class FlamechartRenderer { } }) - this.canvasContext.drawOutlines({ - colorTexture: this.colorTexture, - depthTexture: this.depthTexture, - srcRect: new Rect(Vec2.zero, new Vec2(this.colorTexture.width, this.colorTexture.height)), - dstRect: physicalSpaceDstRect + this.canvasContext.drawFlamechartColorPass({ + rectInfoTexture: this.rectInfoTexture, + srcRect: new Rect(Vec2.zero, new Vec2(this.rectInfoTexture.width, this.rectInfoTexture.height)), + dstRect: physicalSpaceDstRect, + renderOutlines: props.renderOutlines }) - // 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 /* this.canvasContext.drawTexture({ diff --git a/flamechart-view.tsx b/flamechart-view.tsx index c3f2453..693d772 100644 --- a/flamechart-view.tsx +++ b/flamechart-view.tsx @@ -311,7 +311,8 @@ export class FlamechartPanZoomView extends ReloadableComponent { this.props.flamechartRenderer.render({ physicalSpaceDstRect: new Rect(Vec2.zero, this.physicalViewSize()), - configSpaceSrcRect: this.props.configSpaceViewportRect + configSpaceSrcRect: this.props.configSpaceViewportRect, + renderOutlines: true }) }) } diff --git a/flamechart.ts b/flamechart.ts index 66422fd..2ad9e62 100644 --- a/flamechart.ts +++ b/flamechart.ts @@ -1,5 +1,4 @@ import {Frame, CallTreeNode} from './profile' -import { Color } from './color' import { lastOf } from './utils' @@ -23,7 +22,7 @@ interface FlamechartDataSource { closeFrame: (value: number) => void ): void - getColorForFrame(f: Frame): Color + getColorBucketForFrame(f: Frame): number } export class Flamechart { @@ -34,7 +33,7 @@ export class Flamechart { getTotalWeight() { return this.totalWeight } getLayers() { return this.layers } - getColorForFrame(f: Frame) { return this.source.getColorForFrame(f) } + getColorBucketForFrame(frame: Frame) { return this.source.getColorBucketForFrame(frame) } getMinFrameWidth() { return this.minFrameWidth } formatValue(v: number) { return this.source.formatValue(v) } diff --git a/outline-renderer.ts b/outline-renderer.ts deleted file mode 100644 index d352b20..0000000 --- a/outline-renderer.ts +++ /dev/null @@ -1,130 +0,0 @@ -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 6e0321b..02fc461 100644 --- a/rectangle-batch-renderer.ts +++ b/rectangle-batch-renderer.ts @@ -32,21 +32,10 @@ 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) { @@ -89,16 +78,13 @@ export interface RectangleBatchRendererProps { 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! + // We draw the parity / 5 into the depth channel so it can be used in a + // post-processing step to draw boundaries between rectangles. We use 5 + // different values (2 per row) + one for the background, 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; @@ -113,10 +99,9 @@ export class RectangleBatchRenderer { 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, depth / 4.0, 1); + gl_Position = vec4(position, 1, 1); } `, @@ -128,8 +113,9 @@ export class RectangleBatchRenderer { precision mediump float; varying vec3 vColor; varying float vParity; + void main() { - gl_FragColor = vec4(vColor, 1); + gl_FragColor = vec4(vColor.rgb, 1); } `, @@ -169,15 +155,6 @@ export class RectangleBatchRenderer { size: 3, divisor: 1 } - }, - index: (context, props) => { - return { - buffer: props.batch.getIndexBuffer(), - offset: 0, - stride: 4, - size: 1, - divisor: 1 - } } }, @@ -197,11 +174,11 @@ export class RectangleBatchRenderer { }, parityOffset: (context, props) => { - return props.parityOffset || 0 + return props.parityOffset == null ? 0 : props.parityOffset }, parityMin: (context, props) => { - return props.parityMin || 0 + return props.parityMin == null ? 0 : 1 + props.parityMin } }, diff --git a/texture-catched-renderer.ts b/texture-catched-renderer.ts index 30e25f3..3fdf0b9 100644 --- a/texture-catched-renderer.ts +++ b/texture-catched-renderer.ts @@ -155,10 +155,6 @@ 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 { @@ -173,7 +169,6 @@ 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))