diff --git a/canvas-context.ts b/canvas-context.ts index 4d44b1a..0cbb309 100644 --- a/canvas-context.ts +++ b/canvas-context.ts @@ -1,25 +1,37 @@ import * as regl from 'regl' import { RectangleBatchRenderer, RectangleBatch, RectangleBatchRendererProps } from './rectangle-batch-renderer'; import { ViewportRectangleRenderer, ViewportRectangleRendererProps } from './overlay-rectangle-renderer'; +import { TextureCachedRenderer, TextureRenderer } from './texture-catched-renderer' + import { Vec2, Rect } from './math'; type FrameCallback = () => void +interface SetViewportScopeProps { + physicalBounds: Rect +} + export class CanvasContext { private gl: regl.Instance private rectangleBatchRenderer: RectangleBatchRenderer private viewportRectangleRenderer: ViewportRectangleRenderer + private textureRenderer: TextureRenderer private setViewportScope: regl.Command<{ physicalBounds: Rect }> constructor(canvas: HTMLCanvasElement) { this.gl = regl(canvas) this.rectangleBatchRenderer = new RectangleBatchRenderer(this.gl) this.viewportRectangleRenderer = new ViewportRectangleRenderer(this.gl) + this.textureRenderer = new TextureRenderer(this.gl) - this.setViewportScope = this.gl({ + this.setViewportScope = this.gl({ context: { - viewportX: (context, props) => props.physicalBounds.left(), - viewportY: (context, props) => props.physicalBounds.top() + viewportX: (context: regl.Context, props: SetViewportScopeProps) => { + return props.physicalBounds.left() + }, + viewportY: (context: regl.Context, props: SetViewportScopeProps) => { + return props.physicalBounds.top() + } }, viewport: (context, props) => { const { physicalBounds } = props @@ -80,11 +92,21 @@ export class CanvasContext { return new RectangleBatch(this.gl) } + createTextureCachedRenderer(options: { + render(t: T): void + shouldUpdate(oldProps: T, newProps: T): boolean + }): TextureCachedRenderer { + return new TextureCachedRenderer(this.gl, { + ...options, + textureRenderer: this.textureRenderer + }) + } + drawViewportRectangle(props: ViewportRectangleRendererProps){ this.viewportRectangleRenderer.render(props) } - renderInto(el: Element, cb: () => void) { + renderInto(el: Element, cb: (context: regl.Context) => void) { const bounds = el.getBoundingClientRect() const physicalBounds = new Rect( new Vec2(bounds.left * window.devicePixelRatio, bounds.top * window.devicePixelRatio), diff --git a/flamechart-minimap-view.tsx b/flamechart-minimap-view.tsx index f3f751f..3e04e37 100644 --- a/flamechart-minimap-view.tsx +++ b/flamechart-minimap-view.tsx @@ -7,6 +7,7 @@ import { cachedMeasureTextWidth } from "./utils"; import { style, Sizes } from "./flamechart-style"; import { FontFamily, FontSize, Colors } from "./style" import { CanvasContext } from './canvas-context' +import { TextureCachedRenderer } from './texture-catched-renderer' const DEVICE_PIXEL_RATIO = window.devicePixelRatio @@ -77,16 +78,39 @@ export class FlamechartMinimapView extends Component | null = null private renderRects() { if (!this.container) return + if (!this.cachedRenderer) { + this.cachedRenderer = this.props.canvasContext.createTextureCachedRenderer({ + shouldUpdate(oldProps, newProps) { + if (!oldProps.physicalSize.equals(newProps.physicalSize)) { + return true + } else if (!oldProps.configSpaceToNDC.equals(newProps.configSpaceToNDC)) { + return true + } + return false + }, + render: (props) => { + this.props.canvasContext.drawRectangleBatch({ + configSpaceToNDC: props.configSpaceToNDC, + physicalSize: props.physicalSize, + strokeSize: 0, + batch: this.props.rectangles + }) + } + }) + } + const configSpaceToNDC = this.physicalViewSpaceToNDC().times(this.configSpaceToPhysicalViewSpace()) - this.props.canvasContext.renderInto(this.container, () => { - this.props.canvasContext.drawRectangleBatch({ - configSpaceToNDC: configSpaceToNDC, - physicalSize: this.physicalViewSize(), - strokeSize: 0, - batch: this.props.rectangles + this.props.canvasContext.renderInto(this.container, (context) => { + this.cachedRenderer!.render(context, { + configSpaceToNDC, + physicalSize: this.physicalViewSize() }) this.props.canvasContext.drawViewportRectangle({ configSpaceViewportRect: this.props.configSpaceViewportRect, diff --git a/math.ts b/math.ts index 7e76ca7..e0706fa 100644 --- a/math.ts +++ b/math.ts @@ -15,6 +15,7 @@ export class Vec2 { timesPointwise(other: Vec2) { return new Vec2(this.x * other.x, this.y * other.y) } dividedByPointwise(other: Vec2) { return new Vec2(this.x / other.x, this.y / other.y) } dot(other: Vec2) { return this.x * other.x + this.y * other.y } + equals(other: Vec2) { return this.x === other.x && this.y === other.y } length2() { return this.dot(this) } length() { return Math.sqrt(this.length2()) } @@ -86,6 +87,15 @@ export class AffineTransform { return new AffineTransform(m00, m01, m02, m10, m11, m12) } + equals(other: AffineTransform) { + return this.m00 == other.m00 && + this.m01 == other.m01 && + this.m02 == other.m02 && + this.m10 == other.m10 && + this.m11 == other.m11 && + this.m12 == other.m12; + } + timesScalar(s: number) { const {m00, m01, m02, m10, m11, m12} = this return new AffineTransform(s*m00, s*m01, s*m02, s*m10, s*m11, s*m12) @@ -234,4 +244,8 @@ export class Rect { return new Rect(topLeft, bottomRight.minus(topLeft)) } + + equals(other: Rect) { + return this.origin.equals(other.origin) && this.size.equals(other.size) + } } \ No newline at end of file diff --git a/regl.d.ts b/regl.d.ts index ae8a769..9f2e4f4 100644 --- a/regl.d.ts +++ b/regl.d.ts @@ -76,6 +76,7 @@ declare module "regl" { // TODO(jlfwong): read() buffer(args: BufferArgs): Buffer + texture(width: number, height: number): Texture texture(args: TextureArgs): Texture framebuffer(args: FramebufferOptions): Framebuffer renderbuffer(args: RenderBufferOptions): RenderBuffer @@ -213,10 +214,13 @@ declare module "regl" { copy?: boolean - data: TextureData + data?: TextureData } type TextureArgs = TextureData | TextureOptions interface Texture { + width: number + height: number + (args: TextureArgs): void destroy(): void stats: { diff --git a/reloadable.tsx b/reloadable.tsx index dc03770..ef47808 100644 --- a/reloadable.tsx +++ b/reloadable.tsx @@ -1,6 +1,6 @@ import {Component} from 'preact' -interface SerializedComponent { +export interface SerializedComponent { state: S serializedSubcomponents: {[key: string]: any} } diff --git a/texture-catched-renderer.ts b/texture-catched-renderer.ts new file mode 100644 index 0000000..d5cfd27 --- /dev/null +++ b/texture-catched-renderer.ts @@ -0,0 +1,125 @@ +import * as regl from 'regl' + +export class TextureRendererProps { + texture: regl.Texture +} + +export class TextureRenderer { + private command: regl.Command + constructor(gl: regl.Instance) { + this.command = gl({ + vert: ` + attribute vec2 position; + attribute vec2 uv; + varying vec2 vUv; + + void main() { + vUv = uv; + gl_Position = vec4(position, 0, 1); + } + `, + frag: ` + precision mediump float; + + varying vec2 vUv; + uniform sampler2D texture; + + void main() { + gl_FragColor = texture2D(texture, vUv); + } + `, + attributes: { + // Cover full canvas with a rectangle + // with 2 triangles using a triangle + // strip. + // + // 0 +--+ 1 + // | /| + // |/ | + // 2 +--+ 3 + position: [ + [-1, 1], + [1, 1], + [-1, -1], + [1, -1] + ], + uv: [ + [0, 1], + [1, 1], + [0, 0], + [1, 0] + ] + }, + + uniforms: { + texture: (context, props) => props.texture + }, + + primitive: 'triangle strip', + + count: 4 + }) + } + + render(context: regl.Context, props: TextureRendererProps) { + this.command(props) + } +} + +export interface TextureCachedRendererOptions { + textureRenderer: TextureRenderer + render(t: T): void + shouldUpdate(oldProps: T, newProps: T): boolean +} + +export class TextureCachedRenderer { + private renderUncached: (t: T) => void + private shouldUpdate: (oldProps: T, newProps: T) => boolean + private texture: regl.Texture + private framebuffer: regl.Framebuffer + private textureRenderer: TextureRenderer + + constructor(private gl: regl.Instance, options: TextureCachedRendererOptions) { + this.renderUncached = options.render + this.shouldUpdate = options.shouldUpdate + this.textureRenderer = options.textureRenderer + + this.texture = gl.texture(1, 1) + this.framebuffer = gl.framebuffer({color: [this.texture]}) + } + + private lastRenderProps: T | null = null + 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 + } + + 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.renderUncached(props) + }) + } + + // Render from texture + this.textureRenderer.render(context, {texture: this.texture}) + this.lastRenderProps = props + } +} \ No newline at end of file