Add better insights and FPS autoscaling
This commit is contained in:
parent
30296409ac
commit
e5423212c3
11 changed files with 132 additions and 220 deletions
|
|
@ -1,8 +1,8 @@
|
|||
import { Insights } from '../../rendering/insights';
|
||||
import { UniversalRenderingContext } from '../universal-rendering-context';
|
||||
|
||||
/** @internal */
|
||||
export const tryEnableExtension = (
|
||||
gl: WebGL2RenderingContext | WebGLRenderingContext,
|
||||
gl: UniversalRenderingContext,
|
||||
name: string
|
||||
): any | null => {
|
||||
let extension = null;
|
||||
|
|
@ -10,16 +10,13 @@ export const tryEnableExtension = (
|
|||
extension = gl.getExtension(name);
|
||||
}
|
||||
|
||||
Insights.setValue(['extensions', name], extension !== null);
|
||||
gl.insights.extensions[name] = extension !== null;
|
||||
|
||||
return extension;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export const enableExtension = (
|
||||
gl: WebGL2RenderingContext | WebGLRenderingContext,
|
||||
name: string
|
||||
): any => {
|
||||
export const enableExtension = (gl: UniversalRenderingContext, name: string): any => {
|
||||
const extension = tryEnableExtension(gl, name);
|
||||
|
||||
if (extension === null) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { numberToGlslFloat } from '../../helper/number-to-glsl-float';
|
||||
import { wait } from '../../helper/wait';
|
||||
import { Insights } from '../rendering/insights';
|
||||
import { tryEnableExtension } from './helper/enable-extension';
|
||||
import { UniversalRenderingContext } from './universal-rendering-context';
|
||||
|
||||
|
|
@ -60,11 +59,10 @@ export class ParallelCompiler {
|
|||
return promise;
|
||||
}
|
||||
|
||||
@Insights.measure('compile programs')
|
||||
public async compilePrograms(): Promise<void> {
|
||||
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) {
|
||||
this.resolveFinishedPrograms();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Insights } from '../rendering/insights';
|
||||
import { RendererInfo } from '../rendering/renderer/renderer-info';
|
||||
|
||||
/** @internal */
|
||||
export type UniversalRenderingContext =
|
||||
export type UniversalRenderingContext = (
|
||||
| (WebGL2RenderingContext & { isWebGL2: true })
|
||||
| (WebGLRenderingContext & { isWebGL2: false });
|
||||
| (WebGLRenderingContext & { isWebGL2: false })
|
||||
) & { insights: RendererInfo };
|
||||
|
||||
/** @internal */
|
||||
export const getUniversalRenderingContext = (
|
||||
|
|
@ -31,7 +32,14 @@ export const getUniversalRenderingContext = (
|
|||
result.isWebGL2 = false;
|
||||
}
|
||||
|
||||
Insights.setValue('using WebGL2', result.isWebGL2);
|
||||
result.insights = {
|
||||
isWebGL2: result.isWebGL2,
|
||||
extensions: {},
|
||||
renderPasses: {
|
||||
distance: {},
|
||||
lights: {},
|
||||
},
|
||||
};
|
||||
|
||||
let isDestroyed = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/graphics/rendering/fps-quality-autoscaler.ts
Normal file
78
src/graphics/rendering/fps-quality-autoscaler.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
22
src/graphics/rendering/renderer/renderer-info.ts
Normal file
22
src/graphics/rendering/renderer/renderer-info.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue