Render minimap to texture to avoid superfluous re-renders

This commit is contained in:
Jamie Wong
2018-01-23 23:48:13 -08:00
parent 61d321f6d4
commit 767c677824
6 changed files with 201 additions and 12 deletions
+26 -4
View File
@@ -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<SetViewportScopeProps>({
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<T>(options: {
render(t: T): void
shouldUpdate(oldProps: T, newProps: T): boolean
}): TextureCachedRenderer<T> {
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),
+30 -6
View File
@@ -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<FlamechartMinimapViewProps,
return AffineTransform.withTranslation(new Vec2(-bounds.left, -bounds.top))
}
private cachedRenderer: TextureCachedRenderer<{
physicalSize: Vec2,
configSpaceToNDC: AffineTransform
}> | 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,
+14
View File
@@ -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)
}
}
Vendored
+5 -1
View File
@@ -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: {
+1 -1
View File
@@ -1,6 +1,6 @@
import {Component} from 'preact'
interface SerializedComponent<S> {
export interface SerializedComponent<S> {
state: S
serializedSubcomponents: {[key: string]: any}
}
+125
View File
@@ -0,0 +1,125 @@
import * as regl from 'regl'
export class TextureRendererProps {
texture: regl.Texture
}
export class TextureRenderer {
private command: regl.Command<TextureRendererProps>
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<T> {
textureRenderer: TextureRenderer
render(t: T): void
shouldUpdate(oldProps: T, newProps: T): boolean
}
export class TextureCachedRenderer<T> {
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<T>) {
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
}
}