diff --git a/src/graphics/graphics-library/helper/enable-extension.ts b/src/graphics/graphics-library/helper/enable-extension.ts index 8766093..9dc35e2 100644 --- a/src/graphics/graphics-library/helper/enable-extension.ts +++ b/src/graphics/graphics-library/helper/enable-extension.ts @@ -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) { diff --git a/src/graphics/graphics-library/parallel-compiler.ts b/src/graphics/graphics-library/parallel-compiler.ts index d33bac7..d2c743f 100644 --- a/src/graphics/graphics-library/parallel-compiler.ts +++ b/src/graphics/graphics-library/parallel-compiler.ts @@ -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 { 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(); diff --git a/src/graphics/graphics-library/universal-rendering-context.ts b/src/graphics/graphics-library/universal-rendering-context.ts index 4799fcc..6726652 100644 --- a/src/graphics/graphics-library/universal-rendering-context.ts +++ b/src/graphics/graphics-library/universal-rendering-context.ts @@ -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; diff --git a/src/graphics/rendering/fps-autoscaler.ts b/src/graphics/rendering/fps-autoscaler.ts deleted file mode 100644 index fe85aa4..0000000 --- a/src/graphics/rendering/fps-autoscaler.ts +++ /dev/null @@ -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(); - } - } - } -} diff --git a/src/graphics/rendering/fps-quality-autoscaler.ts b/src/graphics/rendering/fps-quality-autoscaler.ts new file mode 100644 index 0000000..7497b04 --- /dev/null +++ b/src/graphics/rendering/fps-quality-autoscaler.ts @@ -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 = []; + 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, + }); + } + } +} diff --git a/src/graphics/rendering/insights.ts b/src/graphics/rendering/insights.ts deleted file mode 100644 index 03eba60..0000000 --- a/src/graphics/rendering/insights.ts +++ /dev/null @@ -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, 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) { - return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { - const targetFunction = descriptor.value; - - descriptor.value = function (...values: Array) { - return Insights.measureFunction(key, () => targetFunction.apply(this, values)); - }; - - return descriptor; - }; - } - - public static measureFunction( - key: string | Array, - 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; - } -} diff --git a/src/graphics/rendering/render-pass/distance-render-pass.ts b/src/graphics/rendering/render-pass/distance-render-pass.ts index a4fc961..864251a 100644 --- a/src/graphics/rendering/render-pass/distance-render-pass.ts +++ b/src/graphics/rendering/render-pass/distance-render-pass.ts @@ -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 = []; } diff --git a/src/graphics/rendering/render-pass/lights-render-pass.ts b/src/graphics/rendering/render-pass/lights-render-pass.ts index cbbd33d..8adcb29 100644 --- a/src/graphics/rendering/render-pass/lights-render-pass.ts +++ b/src/graphics/rendering/render-pass/lights-render-pass.ts @@ -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 = []; } diff --git a/src/graphics/rendering/renderer/context-aware-renderer.ts b/src/graphics/rendering/renderer/context-aware-renderer.ts index accde78..1a67bdc 100644 --- a/src/graphics/rendering/renderer/context-aware-renderer.ts +++ b/src/graphics/rendering/renderer/context-aware-renderer.ts @@ -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), diff --git a/src/graphics/rendering/renderer/renderer-info.ts b/src/graphics/rendering/renderer/renderer-info.ts new file mode 100644 index 0000000..e76db9e --- /dev/null +++ b/src/graphics/rendering/renderer/renderer-info.ts @@ -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; +} diff --git a/src/graphics/rendering/renderer/renderer.ts b/src/graphics/rendering/renderer/renderer.ts index b038238..3b0d620 100644 --- a/src/graphics/rendering/renderer/renderer.ts +++ b/src/graphics/rendering/renderer/renderer.ts @@ -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; }