Partially implement benchmarking for SVG.

This commit is contained in:
Patrick Walton 2017-12-05 19:04:55 -08:00
parent f5a7032ca5
commit 30893fb9ad
10 changed files with 267 additions and 51 deletions

View File

@ -8,7 +8,9 @@
</head>
<body class="pf-unscrollable">
{{>partials/navbar.html isTool=true}}
<canvas id="pf-canvas" class="pf-draggable" width="400" height="300"></canvas>
<canvas id="pf-canvas" class="pf-maximized-canvas pf-draggable" width="400"
height="300"></canvas>
<svg id="pf-svg" xmlns="http://www.w3.org/2000/svg" width="400" height="300"></svg>
<div class="modal fade" id="pf-benchmark-modal" tabindex="-1" role="dialog"
aria-labelledby="pf-benchmark-label" aria-hidden="false">
<div class="modal-dialog" role="document">
@ -21,22 +23,44 @@
</button>
</div>
<div class="modal-body">
<form class="form">
<div class="form-group">
<label for="pf-aa-level-select"
class="col-form-label mr-sm-2">Antialiasing</label>
<select id="pf-aa-level-select"
class="form-control custom-select mr-sm-3">
<option value="none" selected>None</option>
<option value="ssaa-2">2&times;SSAA</option>
<option value="ssaa-4">4&times;SSAA</option>
<option value="xcaa">XCAA</option>
</select>
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="pf-benchmark-text-tab"
data-toggle="tab" href="#pf-benchmark-text" role="tab"
aria-controls="pf-benchmark-text" aria-selected="true">Text</a>
</li>
<li class="nav-item">
<a class="nav-link" id="pf-benchmark-svg-tab" data-toggle="tab"
href="#pf-benchmark-svg" role="tab"
aria-controls="pf-benchmark-svg" aria-selected="false">SVG</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active pt-3" id="pf-benchmark-text"
role="tabpanel" aria-labelledby="pf-benchmark-text-tab">
<form class="form" id="pf-benchmark-text-form">
<div class="form-group" id="pf-aa-level-form-group">
<label for="pf-aa-level-select"
class="col-form-label mr-sm-2">Antialiasing</label>
<select id="pf-aa-level-select"
class="form-control custom-select mr-sm-3">
<option value="none" selected>None</option>
<option value="ssaa-2">2&times;SSAA</option>
<option value="ssaa-4">4&times;SSAA</option>
<option value="xcaa">XCAA</option>
</select>
</div>
<div class="form-group row justify-content-between">
{{>partials/switch.html id="pf-subpixel-aa" title="Subpixel AA"}}
</div>
</form>
</div>
<div class="form-group row justify-content-between">
{{>partials/switch.html id="pf-subpixel-aa" title="Subpixel AA"}}
<div class="tab-pane pt-3" id="pf-benchmark-svg"
role="tabpanel" aria-labelledby="pf-benchmark-svg-tab">
<form class="form" id="pf-benchmark-svg-form">
</form>
</div>
</form>
</div>
<div class="d-flex justify-content-end">
<button id="pf-run-benchmark-button" type="button"
class="btn btn-primary">
@ -67,7 +91,7 @@
<span id="pf-benchmark-results-partitioning-time">0</span> µs/glyph
</div>
<table class="table table-striped">
<thead class="thead-default">
<thead class="thead-default" id="pf-benchmark-results-table-header">
<tr><th>Font size (px)</th><th>GPU time per glyph (µs)</th></tr>
</thead>
<tbody id="pf-benchmark-results-table-body"></tbody>

View File

@ -21,7 +21,7 @@ import PathfinderBufferTexture from "./buffer-texture";
import {CameraView, PerspectiveCamera} from "./camera";
import {UniformMap} from './gl-utils';
import {PathfinderMeshData} from "./meshes";
import {PathTransformBuffers, Renderer} from './renderer';
import {BaseRenderer, PathTransformBuffers} from './renderer';
import {ShaderMap, ShaderProgramSource} from "./shader-loader";
import SSAAStrategy from "./ssaa-strategy";
import {BUILTIN_FONT_URI, ExpandedMeshData} from "./text";
@ -359,7 +359,7 @@ class ThreeDView extends DemoView implements TextRenderContext {
newTimingsReceived(timings: Timings): void {}
}
class ThreeDRenderer extends Renderer {
class ThreeDRenderer extends BaseRenderer {
renderContext: ThreeDView;
camera: PerspectiveCamera;

View File

@ -19,9 +19,11 @@ import PathfinderBufferTexture from './buffer-texture';
import {OrthographicCamera} from './camera';
import {UniformMap} from './gl-utils';
import {PathfinderMeshData} from "./meshes";
import {PathTransformBuffers, Renderer} from './renderer';
import {BaseRenderer, PathTransformBuffers} from './renderer';
import {ShaderMap, ShaderProgramSource} from "./shader-loader";
import SSAAStrategy from './ssaa-strategy';
import {BUILTIN_SVG_URI, SVGLoader} from './svg-loader';
import {SVGRenderer} from './svg-renderer';
import {BUILTIN_FONT_URI, ExpandedMeshData, GlyphStore, PathfinderFont, TextFrame} from "./text";
import {computeStemDarkeningAmount, TextRun} from "./text";
import {assert, lerp, PathfinderError, unwrapNull, unwrapUndef} from "./utils";
@ -30,7 +32,8 @@ import {AdaptiveMonochromeXCAAStrategy} from './xcaa-strategy';
const STRING: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const FONT: string = 'nimbus-sans';
const DEFAULT_FONT: string = 'nimbus-sans';
const DEFAULT_SVG_FILE: string = 'tiger';
const TEXT_COLOR: number[] = [0, 0, 0, 255];
@ -47,22 +50,48 @@ const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
xcaa: AdaptiveMonochromeXCAAStrategy,
};
interface BenchmarkModeMap<T> {
text: T;
svg: T;
}
type BenchmarkMode = 'text' | 'svg';
interface AntialiasingStrategyTable {
none: typeof NoAAStrategy;
ssaa: typeof SSAAStrategy;
xcaa: typeof AdaptiveMonochromeXCAAStrategy;
}
const DISPLAY_HEADER_LABELS: BenchmarkModeMap<string[]> = {
svg: ["Size (px)", "GPU time (ms)"],
text: ["Font size (px)", "GPU time per glyph (µs)"],
};
class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
font: PathfinderFont | null;
textRun: TextRun | null;
protected readonly defaultFile: string = FONT;
protected readonly builtinFileURI: string = BUILTIN_FONT_URI;
svgLoader: SVGLoader;
mode: BenchmarkMode;
protected get defaultFile(): string {
if (this.mode === 'text')
return DEFAULT_FONT;
return DEFAULT_SVG_FILE;
}
protected get builtinFileURI(): string {
if (this.mode === 'text')
return BUILTIN_FONT_URI;
return BUILTIN_SVG_URI;
}
private optionsModal: HTMLDivElement;
private resultsModal: HTMLDivElement;
private resultsTableHeader: HTMLTableSectionElement;
private resultsTableBody: HTMLTableSectionElement;
private resultsPartitioningTimeLabel: HTMLSpanElement;
@ -79,11 +108,16 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
start(): void {
super.start();
this.mode = 'text';
this.optionsModal = unwrapNull(document.getElementById('pf-benchmark-modal')) as
HTMLDivElement;
this.resultsModal = unwrapNull(document.getElementById('pf-benchmark-results-modal')) as
HTMLDivElement;
this.resultsTableHeader =
unwrapNull(document.getElementById('pf-benchmark-results-table-header')) as
HTMLTableSectionElement;
this.resultsTableBody =
unwrapNull(document.getElementById('pf-benchmark-results-table-body')) as
HTMLTableSectionElement;
@ -104,12 +138,56 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
const runBenchmarkButton = unwrapNull(document.getElementById('pf-run-benchmark-button'));
runBenchmarkButton.addEventListener('click', () => this.runBenchmark(), false);
const aaLevelFormGroup = unwrapNull(document.getElementById('pf-aa-level-form-group')) as
HTMLDivElement;
const benchmarkTextForm = unwrapNull(document.getElementById('pf-benchmark-text-form')) as
HTMLFormElement;
const benchmarkSVGForm = unwrapNull(document.getElementById('pf-benchmark-svg-form')) as
HTMLFormElement;
window.jQuery(this.optionsModal).modal();
const benchmarkTextTab = document.getElementById('pf-benchmark-text-tab') as
HTMLAnchorElement;
const benchmarkSVGTab = document.getElementById('pf-benchmark-svg-tab') as
HTMLAnchorElement;
window.jQuery(benchmarkTextTab).on('shown.bs.tab', event => {
this.mode = 'text';
if (aaLevelFormGroup.parentElement != null)
aaLevelFormGroup.parentElement.removeChild(aaLevelFormGroup);
benchmarkTextForm.insertBefore(aaLevelFormGroup, benchmarkTextForm.firstChild);
this.loadInitialFile(this.builtinFileURI);
});
window.jQuery(benchmarkSVGTab).on('shown.bs.tab', event => {
this.mode = 'svg';
if (aaLevelFormGroup.parentElement != null)
aaLevelFormGroup.parentElement.removeChild(aaLevelFormGroup);
benchmarkSVGForm.insertBefore(aaLevelFormGroup, benchmarkSVGForm.firstChild);
this.loadInitialFile(this.builtinFileURI);
});
this.loadInitialFile(this.builtinFileURI);
}
protected fileLoaded(fileData: ArrayBuffer, builtinName: string | null): void {
switch (this.mode) {
case 'text':
this.textFileLoaded(fileData, builtinName);
return;
case 'svg':
this.svgFileLoaded(fileData, builtinName);
return;
}
}
protected createView(gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>):
BenchmarkTestView {
return new BenchmarkTestView(this, gammaLUT, commonShaderSource, shaderSources);
}
private textFileLoaded(fileData: ArrayBuffer, builtinName: string | null): void {
const font = new PathfinderFont(fileData, builtinName);
this.font = font;
@ -135,16 +213,22 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
this.expandedMeshes = expandedMeshes;
this.view.then(view => {
view.recreateRenderer();
view.attachMeshes([expandedMeshes.meshes]);
});
});
}
protected createView(gammaLUT: HTMLImageElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>):
BenchmarkTestView {
return new BenchmarkTestView(this, gammaLUT, commonShaderSource, shaderSources);
private svgFileLoaded(fileData: ArrayBuffer, builtinName: string | null): void {
this.svgLoader = new SVGLoader;
this.svgLoader.loadFile(fileData);
this.svgLoader.partition().then(meshes => {
this.view.then(view => {
view.recreateRenderer();
view.attachMeshes([meshes]);
view.initCameraBounds(this.svgLoader.svgBounds);
});
});
}
private reset(): void {
@ -206,15 +290,26 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
}
private showResults(): void {
while (this.resultsTableHeader.lastChild != null)
this.resultsTableHeader.removeChild(this.resultsTableHeader.lastChild);
while (this.resultsTableBody.lastChild != null)
this.resultsTableBody.removeChild(this.resultsTableBody.lastChild);
const tr = document.createElement('tr');
for (const headerLabel of DISPLAY_HEADER_LABELS[this.mode]) {
const th = document.createElement('th');
th.appendChild(document.createTextNode(headerLabel));
tr.appendChild(th);
}
this.resultsTableHeader.appendChild(tr);
for (const elapsedTime of this.elapsedTimes) {
const tr = document.createElement('tr');
const sizeTH = document.createElement('th');
const timeTD = document.createElement('td');
sizeTH.appendChild(document.createTextNode("" + elapsedTime.size));
timeTD.appendChild(document.createTextNode("" + elapsedTime.time));
const time = this.mode === 'svg' ? elapsedTime.timeInMS : elapsedTime.time;
timeTD.appendChild(document.createTextNode("" + time));
sizeTH.scope = 'row';
tr.appendChild(sizeTH);
tr.appendChild(timeTD);
@ -246,7 +341,8 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
}
class BenchmarkTestView extends DemoView {
readonly renderer: BenchmarkRenderer;
renderer: BenchmarkTextRenderer | BenchmarkSVGRenderer;
readonly appController: BenchmarkAppController;
renderingPromiseCallback: ((time: number) => void) | null;
@ -256,7 +352,14 @@ class BenchmarkTestView extends DemoView {
}
set pixelsPerEm(newPPEM: number) {
this.renderer.pixelsPerEm = newPPEM;
if (this.renderer instanceof BenchmarkTextRenderer) {
this.renderer.pixelsPerEm = newPPEM;
} else if (this.renderer instanceof BenchmarkSVGRenderer) {
const camera = this.renderer.camera;
camera.reset();
camera.zoom(newPPEM / 100.0);
camera.center();
}
}
constructor(appController: BenchmarkAppController,
@ -264,24 +367,40 @@ class BenchmarkTestView extends DemoView {
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>) {
super(gammaLUT, commonShaderSource, shaderSources);
this.appController = appController;
this.renderer = new BenchmarkRenderer(this);
this.recreateRenderer();
this.resizeToFit(true);
}
recreateRenderer(): void {
switch (this.appController.mode) {
case 'svg':
this.renderer = new BenchmarkSVGRenderer(this);
break;
case 'text':
this.renderer = new BenchmarkTextRenderer(this);
break;
}
}
initCameraBounds(bounds: glmatrix.vec4): void {
if (this.renderer instanceof BenchmarkSVGRenderer)
this.renderer.initCameraBounds(bounds);
}
protected renderingFinished(): void {
if (this.renderingPromiseCallback == null)
return;
const glyphCount = unwrapNull(this.appController.textRun).glyphIDs.length;
const usPerGlyph = this.renderer.lastTimings.rendering * 1000.0 / glyphCount;
this.renderingPromiseCallback(usPerGlyph);
const appController = this.appController;
let time = this.renderer.lastTimings.rendering * 1000.0;
if (appController.mode === 'text')
time /= unwrapNull(appController.textRun).glyphIDs.length;
this.renderingPromiseCallback(time);
}
}
class BenchmarkRenderer extends Renderer {
class BenchmarkTextRenderer extends BaseRenderer {
renderContext: BenchmarkTestView;
camera: OrthographicCamera;
@ -459,6 +578,22 @@ class BenchmarkRenderer extends Renderer {
}
}
class BenchmarkSVGRenderer extends SVGRenderer {
renderContext: BenchmarkTestView;
protected get loader(): SVGLoader {
return this.renderContext.appController.svgLoader;
}
protected get canvas(): HTMLCanvasElement {
return this.renderContext.canvas;
}
constructor(renderContext: BenchmarkTestView) {
super(renderContext);
}
}
function computeMedian(values: number[]): number | null {
if (values.length === 0)
return null;
@ -481,6 +616,10 @@ class ElapsedTime {
const median = computeMedian(this.times);
return median == null ? 0.0 : median;
}
get timeInMS(): number {
return this.time / 1000.0;
}
}
function main() {

View File

@ -123,9 +123,7 @@ export class OrthographicCamera extends Camera {
this.scaleBounds = !!options.scaleBounds;
this.ignoreBounds = !!options.ignoreBounds;
this.translation = glmatrix.vec2.create();
this.scale = 1.0;
this.rotationAngle = 0.0;
this.reset();
this._bounds = glmatrix.vec4.create();
@ -139,6 +137,12 @@ export class OrthographicCamera extends Camera {
this.onRotate = null;
}
reset(): void {
this.translation = glmatrix.vec2.create();
this.scale = 1.0;
this.rotationAngle = 0.0;
}
onWheel(event: MouseWheelEvent): void {
if (this.canvas == null)
throw new Error("onWheel() with no canvas?!");
@ -174,6 +178,13 @@ export class OrthographicCamera extends Camera {
this.scale = Math.min(this.canvas.width / width, this.canvas.height / height);
// Center.
this.center();
}
center(): void {
const upperLeft = glmatrix.vec2.clone([this._bounds[0], this._bounds[1]]);
const lowerRight = glmatrix.vec2.clone([this._bounds[2], this._bounds[3]]);
this.translation = glmatrix.vec2.create();
glmatrix.vec2.lerp(this.translation, upperLeft, lowerRight, 0.5);
glmatrix.vec2.scale(this.translation, this.translation, -this.scale);
@ -269,7 +280,7 @@ export class OrthographicCamera extends Camera {
}
private get centerPoint(): glmatrix.vec2 {
return glmatrix.vec2.fromValues(this.canvas.width * 0.5, this.canvas.height * 0.5);
return glmatrix.vec2.clone([this.canvas.width * 0.5, this.canvas.height * 0.5]);
}
get bounds(): glmatrix.vec4 {

View File

@ -20,7 +20,7 @@ import {SUBPIXEL_GRANULARITY} from './atlas';
import {OrthographicCamera} from './camera';
import {UniformMap} from './gl-utils';
import {PathfinderMeshData} from './meshes';
import {PathTransformBuffers, Renderer} from "./renderer";
import {BaseRenderer, PathTransformBuffers} from "./renderer";
import {ShaderMap, ShaderProgramSource} from "./shader-loader";
import SSAAStrategy from './ssaa-strategy';
import {BUILTIN_FONT_URI, computeStemDarkeningAmount, ExpandedMeshData, GlyphStore} from "./text";
@ -465,7 +465,7 @@ class ReferenceTestView extends DemoView {
}
}
class ReferenceTestRenderer extends Renderer {
class ReferenceTestRenderer extends BaseRenderer {
renderContext: ReferenceTestView;
camera: OrthographicCamera;

View File

@ -36,7 +36,43 @@ export interface PathTransformBuffers<T> {
ext: T;
}
export abstract class Renderer {
export interface Renderer {
readonly renderContext: RenderContext;
readonly pathTransformBufferTextures: Array<PathTransformBuffers<PathfinderBufferTexture>>;
readonly meshes: PathfinderMeshBuffers[] | null;
readonly meshData: PathfinderMeshData[] | null;
readonly usesSTTransform: boolean;
readonly emboldenAmount: glmatrix.vec2;
readonly bgColor: glmatrix.vec4;
readonly fgColor: glmatrix.vec4 | null;
readonly backgroundColor: glmatrix.vec4;
readonly meshesAttached: boolean;
readonly usesIntermediateRenderTargets: boolean;
readonly destFramebuffer: WebGLFramebuffer | null;
readonly destAllocatedSize: glmatrix.vec2;
readonly destUsedSize: glmatrix.vec2;
attachMeshes(meshes: PathfinderMeshData[]): void;
pathBoundingRects(objectIndex: number): Float32Array;
setHintsUniform(uniforms: UniformMap): void;
setPathColorsUniform(objectIndex: number, uniforms: UniformMap, textureUnit: number): void;
setEmboldenAmountUniform(objectIndex: number, uniforms: UniformMap): void;
setAntialiasingOptions(aaType: AntialiasingStrategyName,
aaLevel: number,
aaOptions: AAOptions):
void;
redraw(): void;
canvasResized(): void;
meshIndexForObject(objectIndex: number): number;
pathRangeForObject(objectIndex: number): Range;
renderTaskTypeForObject(objectIndex: number): RenderTaskType;
compositingOperationForObject(objectIndex: number): CompositingOperation | null;
setTransformAndTexScaleUniformsForDest(uniforms: UniformMap): void;
setTransformSTAndTexScaleUniformsForDest(uniforms: UniformMap): void;
setTransformUniform(uniforms: UniformMap, objectIndex: number): void;
setTransformSTUniform(uniforms: UniformMap, objectIndex: number): void;
}
export abstract class BaseRenderer implements Renderer {
readonly renderContext: RenderContext;
readonly pathTransformBufferTextures: Array<PathTransformBuffers<PathfinderBufferTexture>>;
@ -240,7 +276,7 @@ export abstract class Renderer {
gl.uniform2f(uniforms.uTexScale, usedSize[0], usedSize[1]);
}
setTransformUniform(uniforms: UniformMap, objectIndex: number) {
setTransformUniform(uniforms: UniformMap, objectIndex: number): void {
const transform = glmatrix.mat4.clone(this.worldTransform);
glmatrix.mat4.mul(transform, transform, this.getModelviewTransform(objectIndex));
this.renderContext.gl.uniformMatrix4fv(uniforms.uTransform, false, transform);

View File

@ -16,7 +16,7 @@ import {OrthographicCamera} from "./camera";
import {UniformMap} from './gl-utils';
import {PathfinderMeshData} from './meshes';
import {CompositingOperation, RenderTaskType} from './render-task';
import {PathTransformBuffers, Renderer} from "./renderer";
import {BaseRenderer, PathTransformBuffers} from "./renderer";
import {ShaderMap} from './shader-loader';
import SSAAStrategy from './ssaa-strategy';
import {SVGLoader} from './svg-loader';
@ -36,7 +36,7 @@ const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
xcaa: ECAAMulticolorStrategy,
};
export abstract class SVGRenderer extends Renderer {
export abstract class SVGRenderer extends BaseRenderer {
renderContext: RenderContext;
camera: OrthographicCamera;

View File

@ -24,7 +24,6 @@ import {createFramebuffer, createFramebufferColorTexture} from './gl-utils';
import {createFramebufferDepthTexture, QUAD_ELEMENTS, setTextureParameters} from './gl-utils';
import {UniformMap} from './gl-utils';
import {PathfinderMeshBuffers, PathfinderMeshData} from './meshes';
import {Renderer} from './renderer';
import {PathfinderShaderProgram, ShaderMap, ShaderProgramSource} from './shader-loader';
import SSAAStrategy from './ssaa-strategy';
import {calculatePixelRectForGlyph, PathfinderFont} from "./text";
@ -86,8 +85,14 @@ declare global {
}
}
interface TabShownEvent {
target: EventTarget;
relatedTarget: EventTarget;
}
interface JQuerySubset {
modal(options?: any): void;
on(name: 'shown.bs.tab', handler: (event: TabShownEvent) => void): void;
}
type Matrix4D = Float32Array;

View File

@ -17,7 +17,7 @@ import {Atlas, ATLAS_SIZE, AtlasGlyph, GlyphKey, SUBPIXEL_GRANULARITY} from './a
import {CameraView, OrthographicCamera} from './camera';
import {createFramebuffer, createFramebufferDepthTexture, QUAD_ELEMENTS} from './gl-utils';
import {UniformMap} from './gl-utils';
import {PathTransformBuffers, Renderer} from './renderer';
import {BaseRenderer, PathTransformBuffers} from './renderer';
import {ShaderMap} from './shader-loader';
import SSAAStrategy from './ssaa-strategy';
import {calculatePixelRectForGlyph, computeStemDarkeningAmount, GlyphStore, Hint} from "./text";
@ -56,7 +56,7 @@ export interface TextRenderContext extends RenderContext {
newTimingsReceived(timings: Timings): void;
}
export abstract class TextRenderer extends Renderer {
export abstract class TextRenderer extends BaseRenderer {
renderContext: TextRenderContext;
camera: OrthographicCamera;

View File

@ -787,7 +787,8 @@ export abstract class ECAAStrategy extends XCAAStrategy {
this.curveShaderProgramNames[1]);
}
protected setAAUniforms(renderer: Renderer, uniforms: UniformMap, objectIndex: number): void {
protected setAAUniforms(renderer: Renderer, uniforms: UniformMap, objectIndex: number):
void {
super.setAAUniforms(renderer, uniforms, objectIndex);
renderer.setEmboldenAmountUniform(objectIndex, uniforms);
}