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

View file

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

View file

@ -5,6 +5,7 @@ import { RuntimeSettings } from '../settings/runtime-settings';
import { StartupSettings } from '../settings/startup-settings';
import { Renderer } from './renderer';
import { RendererImplementation } from './renderer-implementation';
import { RendererInfo } from './renderer-info';
/** @internal */
export class ContextAwareRenderer implements Renderer {
@ -109,8 +110,8 @@ export class ContextAwareRenderer implements Renderer {
return this.handle(() => this.renderer.viewAreaSize, vec2.create());
}
public get insights(): any {
return this.handle(() => this.renderer.insights, {});
public get insights(): RendererInfo | null {
return this.handle(() => this.renderer.insights, null);
}
public setViewArea(topLeft: vec2, size: vec2): void {
@ -132,10 +133,6 @@ export class ContextAwareRenderer implements Renderer {
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 {
return this.handle(
() => 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 { Drawable } from '../../../drawables/drawable';
import { RuntimeSettings } from '../settings/runtime-settings';
import { RendererInfo } from './renderer-info';
/**
* The main interface through which rendering can be achieved.
@ -84,21 +85,12 @@ export interface Renderer {
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;
/**
* @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;
readonly insights: RendererInfo | null;
}