Add better insights and FPS autoscaling

This commit is contained in:
schmelczerandras 2020-10-19 11:56:38 +02:00
parent 30296409ac
commit e5423212c3
11 changed files with 132 additions and 220 deletions

View file

@ -1,8 +1,8 @@
import { Insights } from '../../rendering/insights'; import { UniversalRenderingContext } from '../universal-rendering-context';
/** @internal */ /** @internal */
export const tryEnableExtension = ( export const tryEnableExtension = (
gl: WebGL2RenderingContext | WebGLRenderingContext, gl: UniversalRenderingContext,
name: string name: string
): any | null => { ): any | null => {
let extension = null; let extension = null;
@ -10,16 +10,13 @@ export const tryEnableExtension = (
extension = gl.getExtension(name); extension = gl.getExtension(name);
} }
Insights.setValue(['extensions', name], extension !== null); gl.insights.extensions[name] = extension !== null;
return extension; return extension;
}; };
/** @internal */ /** @internal */
export const enableExtension = ( export const enableExtension = (gl: UniversalRenderingContext, name: string): any => {
gl: WebGL2RenderingContext | WebGLRenderingContext,
name: string
): any => {
const extension = tryEnableExtension(gl, name); const extension = tryEnableExtension(gl, name);
if (extension === null) { if (extension === null) {

View file

@ -1,6 +1,5 @@
import { numberToGlslFloat } from '../../helper/number-to-glsl-float'; import { numberToGlslFloat } from '../../helper/number-to-glsl-float';
import { wait } from '../../helper/wait'; import { wait } from '../../helper/wait';
import { Insights } from '../rendering/insights';
import { tryEnableExtension } from './helper/enable-extension'; import { tryEnableExtension } from './helper/enable-extension';
import { UniversalRenderingContext } from './universal-rendering-context'; import { UniversalRenderingContext } from './universal-rendering-context';
@ -60,11 +59,10 @@ export class ParallelCompiler {
return promise; return promise;
} }
@Insights.measure('compile programs')
public async compilePrograms(): Promise<void> { public async compilePrograms(): Promise<void> {
this.programs.forEach((p) => this.gl.linkProgram(p.program)); this.programs.forEach((p) => this.gl.linkProgram(p.program));
Insights.setValue('program count', this.programs.length); this.gl.insights.programCount = this.programs.length;
while (this.programs.length > 0) { while (this.programs.length > 0) {
this.resolveFinishedPrograms(); this.resolveFinishedPrograms();

View file

@ -1,9 +1,10 @@
import { Insights } from '../rendering/insights'; import { RendererInfo } from '../rendering/renderer/renderer-info';
/** @internal */ /** @internal */
export type UniversalRenderingContext = export type UniversalRenderingContext = (
| (WebGL2RenderingContext & { isWebGL2: true }) | (WebGL2RenderingContext & { isWebGL2: true })
| (WebGLRenderingContext & { isWebGL2: false }); | (WebGLRenderingContext & { isWebGL2: false })
) & { insights: RendererInfo };
/** @internal */ /** @internal */
export const getUniversalRenderingContext = ( export const getUniversalRenderingContext = (
@ -31,7 +32,14 @@ export const getUniversalRenderingContext = (
result.isWebGL2 = false; result.isWebGL2 = false;
} }
Insights.setValue('using WebGL2', result.isWebGL2); result.insights = {
isWebGL2: result.isWebGL2,
extensions: {},
renderPasses: {
distance: {},
lights: {},
},
};
let isDestroyed = false; let isDestroyed = false;

View file

@ -1,110 +0,0 @@
import { clamp } from '../../helper/clamp';
import { exponentialDecay } from '../../helper/exponential-decay';
import { mix } from '../../helper/mix';
import { Insights } from './insights';
/** @internal */
const settings = {
targetDeltaTimeInMilliseconds: 20,
deltaTimeErrorInMilliseconds: 3,
deltaTimeResponsiveness: 1 / 32,
adjusmentRateInMilliseconds: 1000,
targets: [
{
distanceRenderScale: 0.1,
finalRenderScale: 0.2,
},
{
distanceRenderScale: 0.1,
finalRenderScale: 0.6,
},
{
distanceRenderScale: 0.5,
finalRenderScale: 1.0,
},
{
distanceRenderScale: 0.5,
finalRenderScale: 1.0,
},
],
qualityStepIncrease: 0.01,
qualityStepDecrese: 0.2,
};
/** @internal */
export class FpsAutoscaler {
private timeSinceLastAdjusment = 0;
private exponentialDecayedDeltaTime = settings.targetDeltaTimeInMilliseconds;
// can have fractions
private index = 3;
constructor(private setters: { [key: string]: (value: number | boolean) => void }) {
this.applyScaling();
}
public increase() {
this.index += settings.qualityStepIncrease;
this.applyScaling();
}
public decrease() {
this.index -= settings.qualityStepDecrese;
this.applyScaling();
}
private applyScaling() {
this.index = clamp(this.index, 0, settings.targets.length - 1);
const floor = Math.floor(this.index);
const fract = this.index - floor;
const previousTarget: any = settings.targets[floor];
const nextTarget =
floor + 1 == settings.targets.length ? previousTarget : settings.targets[floor + 1];
const result: any = {};
for (const key in this.setters) {
const previous = previousTarget[key];
const next = nextTarget[key];
let current: number | boolean;
if (typeof previous == 'number') {
current = mix(previous, next as number, fract);
} else {
current = next;
}
result[key] = current;
this.setters[key](current);
}
Insights.setValue('quality', result);
}
public autoscale(lastDeltaTime: DOMHighResTimeStamp) {
this.exponentialDecayedDeltaTime = exponentialDecay(
this.exponentialDecayedDeltaTime,
lastDeltaTime,
settings.deltaTimeResponsiveness
);
Insights.setValue('FPS', 1000 / this.exponentialDecayedDeltaTime);
if (
(this.timeSinceLastAdjusment += lastDeltaTime) >=
settings.adjusmentRateInMilliseconds
) {
this.timeSinceLastAdjusment = 0;
if (
this.exponentialDecayedDeltaTime + settings.deltaTimeErrorInMilliseconds <=
settings.targetDeltaTimeInMilliseconds
) {
this.increase();
} else if (
this.exponentialDecayedDeltaTime > settings.targetDeltaTimeInMilliseconds
) {
this.decrease();
}
}
}
}

View file

@ -0,0 +1,78 @@
import { Renderer } from './renderer/renderer';
/**
* Set the quality of rendering based on FPS values.
*
* When using this the size of the canvas must be fixed with CSS.
*
* The `addDeltaTime` method should be called once every frame.
*
* Usage:
* ```js
* const renderer = await compile(...);
* const autoscaler = new FpsQualityAutoscaler(renderer);
* ```
*/
export class FpsQualityAutoscaler {
private readonly maxAdjusmentRateInMilliseconds = 10000;
private readonly adjusmentRateIncrease = 1.3;
private adjusmentRateInMilliseconds = 500;
private fps = 0;
public fpsTarget = 50;
public fpsHysteresis = 5;
constructor(private readonly renderer: Renderer) {}
public get FPS(): number {
return this.fps;
}
private deltaTimes: Array<number> = [];
private deltaTimeSinceLastAdjustment = 0;
/**
* Autoscaling is also done by calling this function
* @param deltaTimeInMilliseconds
*/
public addDeltaTime(deltaTimeInMilliseconds: DOMHighResTimeStamp) {
this.deltaTimes.push(deltaTimeInMilliseconds);
this.deltaTimeSinceLastAdjustment += deltaTimeInMilliseconds;
if (this.deltaTimeSinceLastAdjustment > this.adjusmentRateInMilliseconds) {
this.calculateFPS();
this.adjustQuality();
this.adjusmentRateInMilliseconds = Math.min(
this.maxAdjusmentRateInMilliseconds,
this.adjusmentRateInMilliseconds * this.adjusmentRateIncrease
);
this.deltaTimeSinceLastAdjustment = 0;
}
}
private calculateFPS() {
const sampleCount = this.deltaTimes.length;
this.deltaTimes.sort((a, b) => a - b);
const ninetiethPercentile = this.deltaTimes[Math.floor(sampleCount * 0.9)];
this.deltaTimes = [];
this.fps = 1000 / ninetiethPercentile;
}
private distanceScale = 0.5;
private lightsScale = 1;
private adjustQuality() {
console.log(this.distanceScale, this.lightsScale);
if (this.fps >= this.fpsTarget + this.fpsHysteresis) {
this.renderer.setRuntimeSettings({
distanceRenderScale: this.distanceScale += 0.1,
lightsRenderScale: this.lightsScale += 0.1,
});
} else if (this.fps <= this.fpsTarget + this.fpsHysteresis) {
this.renderer.setRuntimeSettings({
distanceRenderScale: this.distanceScale *= 0.7,
lightsRenderScale: this.lightsScale *= 0.7,
});
}
}
}

View file

@ -1,57 +0,0 @@
import { last } from '../../helper/last';
import { msToString } from '../../helper/ms-to-string';
/** @internal */
export class Insights {
private static insights: any = {};
public static setValue(key: string | Array<string>, value: any): void {
if (Array.isArray(key)) {
if (key.length == 0) {
throw new Error('Key length must be greater than 0');
}
const lastObject = key.slice(0, -1).reduce((previousValue, currentKey) => {
if (!Object.prototype.hasOwnProperty.call(previousValue, currentKey)) {
previousValue[currentKey] = {};
}
return previousValue[currentKey];
}, Insights.insights);
lastObject[last(key)!] = value;
} else {
Insights.insights[key] = value;
}
}
public static measure(key: string | Array<string>) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const targetFunction = descriptor.value;
descriptor.value = function (...values: Array<any>) {
return Insights.measureFunction(key, () => targetFunction.apply(this, values));
};
return descriptor;
};
}
public static measureFunction(
key: string | Array<string>,
targetFunction: () => any
): any {
const start = performance.now();
const result = targetFunction();
const end = performance.now();
const newKey = Array.isArray(key) ? ['measurements', ...key] : ['measurements', key];
Insights.setValue(newKey, msToString(end - start));
return result;
}
public static get values(): any {
return Insights.insights;
}
}

View file

@ -1,7 +1,6 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { Drawable } from '../../../drawables/drawable'; import { Drawable } from '../../../drawables/drawable';
import { Texture } from '../../graphics-library/texture/texture'; import { Texture } from '../../graphics-library/texture/texture';
import { Insights } from '../insights';
import { RenderPass } from './render-pass'; import { RenderPass } from './render-pass';
/** @internal */ /** @internal */
@ -74,18 +73,10 @@ export class DistanceRenderPass extends RenderPass {
} }
} }
Insights.setValue( this.gl.insights.renderPasses.distance.drawableCount = this.drawables.length;
['render pass', 'distance', 'all drawables'], this.gl.insights.renderPasses.distance.drawnDrawableCount =
this.drawables.length drawnDrawablesCount / this.tileMultiplier ** 2;
); this.gl.insights.renderPasses.distance.tileCount = this.tileMultiplier ** 2;
Insights.setValue(
['render pass', 'distance', 'tile count'],
this.tileMultiplier ** 2
);
Insights.setValue(
['render pass', 'distance', 'rendered drawables'],
drawnDrawablesCount / this.tileMultiplier ** 2
);
this.drawables = []; this.drawables = [];
} }

View file

@ -2,7 +2,6 @@ import { vec2 } from 'gl-matrix';
import { LightDrawable } from '../../../drawables/lights/light-drawable'; import { LightDrawable } from '../../../drawables/lights/light-drawable';
import { clamp01 } from '../../../helper/clamp'; import { clamp01 } from '../../../helper/clamp';
import { Texture } from '../../graphics-library/texture/texture'; import { Texture } from '../../graphics-library/texture/texture';
import { Insights } from '../insights';
import { RenderPass } from './render-pass'; import { RenderPass } from './render-pass';
/** @internal */ /** @internal */
@ -50,10 +49,7 @@ export class LightsRenderPass extends RenderPass {
this.program.draw(commonUniforms); this.program.draw(commonUniforms);
Insights.setValue( this.gl.insights.renderPasses.lights.drawnDrawableCount = drawablesNearTile.length;
['render pass', 'lights', 'rendered drawables'],
drawablesNearTile.length
);
this.drawables = []; this.drawables = [];
} }

View file

@ -5,6 +5,7 @@ import { RuntimeSettings } from '../settings/runtime-settings';
import { StartupSettings } from '../settings/startup-settings'; import { StartupSettings } from '../settings/startup-settings';
import { Renderer } from './renderer'; import { Renderer } from './renderer';
import { RendererImplementation } from './renderer-implementation'; import { RendererImplementation } from './renderer-implementation';
import { RendererInfo } from './renderer-info';
/** @internal */ /** @internal */
export class ContextAwareRenderer implements Renderer { export class ContextAwareRenderer implements Renderer {
@ -109,8 +110,8 @@ export class ContextAwareRenderer implements Renderer {
return this.handle(() => this.renderer.viewAreaSize, vec2.create()); return this.handle(() => this.renderer.viewAreaSize, vec2.create());
} }
public get insights(): any { public get insights(): RendererInfo | null {
return this.handle(() => this.renderer.insights, {}); return this.handle(() => this.renderer.insights, null);
} }
public setViewArea(topLeft: vec2, size: vec2): void { public setViewArea(topLeft: vec2, size: vec2): void {
@ -132,10 +133,6 @@ export class ContextAwareRenderer implements Renderer {
return this.handle(() => this.renderer.addDrawable(drawable), undefined); return this.handle(() => this.renderer.addDrawable(drawable), undefined);
} }
public autoscaleQuality(deltaTime: number): void {
return this.handle(() => this.renderer.autoscaleQuality(deltaTime), undefined);
}
public displayToWorldCoordinates(displayCoordinates: vec2): vec2 { public displayToWorldCoordinates(displayCoordinates: vec2): vec2 {
return this.handle( return this.handle(
() => this.renderer.displayToWorldCoordinates(displayCoordinates), () => this.renderer.displayToWorldCoordinates(displayCoordinates),

View file

@ -0,0 +1,22 @@
export interface RendererInfo {
isWebGL2: boolean;
vendor?: string;
renderer?: string;
extensions: { [name: string]: boolean };
floatInterpolationEnabled?: boolean;
programCount?: number;
renderPasses: {
distance: {
renderScale?: number;
drawableCount?: number;
drawnDrawableCount?: number;
tileCount?: number;
};
lights: {
renderScale?: number;
drawnDrawableCount?: number;
};
};
startupTimeInMilliseconds?: number;
[k: string]: any;
}

View file

@ -1,6 +1,7 @@
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
import { Drawable } from '../../../drawables/drawable'; import { Drawable } from '../../../drawables/drawable';
import { RuntimeSettings } from '../settings/runtime-settings'; import { RuntimeSettings } from '../settings/runtime-settings';
import { RendererInfo } from './renderer-info';
/** /**
* The main interface through which rendering can be achieved. * The main interface through which rendering can be achieved.
@ -84,21 +85,12 @@ export interface Renderer {
destroy(): void; destroy(): void;
/** /**
* @experimental * Get useful information about the hardware and the SDF2D renderer.
*
* Its sheme is subject to change.
*
* During context lost it might be null.
* *
* Debug information updated on each `renderDrawables` call.
* Its scheme is not yet defined. The main purpose of this is
* human debugging.
*/ */
readonly insights: any; readonly insights: RendererInfo | null;
/**
* @experimental
*
* Scale the render scale for both the canvas and the SDF memoization based
* on the current and historical FPS values.
*
* @param deltaTime since the last frame, in milliseconds.
*/
autoscaleQuality(deltaTime: DOMHighResTimeStamp): void;
} }