Render minimap to texture to avoid superfluous re-renders
This commit is contained in:
+26
-4
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
import {Component} from 'preact'
|
||||
|
||||
interface SerializedComponent<S> {
|
||||
export interface SerializedComponent<S> {
|
||||
state: S
|
||||
serializedSubcomponents: {[key: string]: any}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user