aboutsummaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/benchmarkCharts.ts90
-rw-r--r--src/ui/benchmarkView.ts111
-rw-r--r--src/ui/demoView.ts141
-rw-r--r--src/ui/index.ts51
4 files changed, 393 insertions, 0 deletions
diff --git a/src/ui/benchmarkCharts.ts b/src/ui/benchmarkCharts.ts
new file mode 100644
index 0000000..85513c2
--- /dev/null
+++ b/src/ui/benchmarkCharts.ts
@@ -0,0 +1,90 @@
+import * as echarts from 'echarts';
+import {EChartsOption, EChartsType} from "echarts";
+
+let scoreChart: EChartsType;
+const userDeviceLabel = 'Your device';
+
+let deviceScores = [
+ { device: 'AMD Ryzen 7\n3700X\n(8c/16t)', multiCoreScore: 416, singleCoreScore: 74 },
+ { device: 'Intel 11th Gen\ni5-1145G7\n(4c/8t)', multiCoreScore: 294, singleCoreScore: 73 },
+ { device: 'Intel 8th Gen\ni7-8550U\n(4c/8t)', multiCoreScore: 221, singleCoreScore: 53 },
+ { device: 'iPhone XS\n(6c/6t)\n(*WebKit)', multiCoreScore: 71, singleCoreScore: 28 },
+ { device: 'Raspberry Pi4\nARM Cortex-A72\n(4c/4t)', multiCoreScore: 25, singleCoreScore: 9 }
+]
+
+let scoreDataset = {
+ dimensions: ['device', 'multiCoreScore', 'singleCoreScore'],
+ source: deviceScores
+}
+
+const option: EChartsOption = {
+ legend: {},
+ dataset: scoreDataset,
+ xAxis: { type: 'value' },
+ yAxis: {
+ type: 'category',
+ inverse: true,
+ axisLabel: {
+ formatter: function (label) {
+ if (label == userDeviceLabel) {
+ return `{usersDevice|${label}}`;
+ }
+ return label;
+ },
+ rich: {
+ usersDevice: {
+ color: "#5755d9",
+ fontWeight: "bold",
+ fontSize: "14px"
+ },
+ },
+ },
+ },
+ series: [
+ {
+ name: 'Multi-core',
+ type: 'bar',
+ label: {
+ show: true
+ },
+ },
+ {
+ name: 'Single core',
+ type: 'bar',
+ label: {
+ show: true
+ },
+ }
+ ],
+ grid: {
+ left: '0%',
+ top: '5%',
+ containLabel: true
+ },
+ color: [
+ '#5755d9',
+ '#f1f1fc'
+ ],
+}
+
+export function initScoreChart(dom: HTMLElement) {
+ scoreChart = echarts.init(dom);
+ scoreChart.setOption(option);
+
+ window.addEventListener('resize', function() {
+ scoreChart.resize();
+ });
+}
+
+export function addDeviceResult(multiCoreScore: number, singleCoreScore: number) {
+ // Remove previous result if present
+ const index = deviceScores.findIndex(d => d.device === userDeviceLabel);
+ if (index >= 0) {
+ deviceScores.splice(index, 1)
+ }
+
+ deviceScores.push({device: userDeviceLabel, multiCoreScore: multiCoreScore, singleCoreScore: singleCoreScore});
+ deviceScores.sort((a, b) => b.multiCoreScore - a.multiCoreScore);
+
+ scoreChart.setOption(option);
+} \ No newline at end of file
diff --git a/src/ui/benchmarkView.ts b/src/ui/benchmarkView.ts
new file mode 100644
index 0000000..487ec63
--- /dev/null
+++ b/src/ui/benchmarkView.ts
@@ -0,0 +1,111 @@
+import {ChunkAllocationMode, RaytraceContext, RaytracerOptions} from "../models/RaytraceContext";
+import {RaytraceDispatcher} from "../RaytraceDispatcher";
+import {Framebuffer} from "../Framebuffer";
+import {getScene} from "../models/Scene";
+import {Colour} from "../models/Colour";
+import {Logger} from "../Logger";
+import * as BenchmarkChart from "./benchmarkCharts";
+
+let multiCoreDispatcher: RaytraceDispatcher;
+let singleCoreDispatcher: RaytraceDispatcher;
+
+let multiCoreScore = 0;
+let singleCoreScore = 0;
+
+const deviceAvailableThreadCount = navigator.hardwareConcurrency;
+
+const logger = new Logger(document.getElementById('console-benchmark')!);
+
+const benchmarkButton = document.getElementById('benchmark')!;
+const multiCoreScoreElement = document.getElementById('multi-core-score')!;
+const singleCoreScoreElement = document.getElementById('single-core-score')!;
+
+export function registerEventListeners() {
+ benchmarkButton.addEventListener('click', startBenchmark);
+ logger.log(`Detected ${deviceAvailableThreadCount} available CPU threads, ready to run`);
+}
+
+export function initChart() {
+ BenchmarkChart.initScoreChart(document.getElementById('benchmark-results-graph')!);
+}
+
+function startBenchmark() {
+ benchmarkButton.classList.add('loading');
+ multiCoreScoreElement.classList.add('loading');
+ document.getElementById('single-core-score')!.classList.add('loading');
+
+ multiCoreDispatcher = initDispatcher(
+ 1920,
+ 1080,
+ getBenchmarkRenderOptions(true),
+ onMultiCoreBenchmarkComplete
+ )
+
+ singleCoreDispatcher = initDispatcher(
+ 1920,
+ 1080,
+ getBenchmarkRenderOptions(false),
+ onSingleCoreBenchmarkComplete
+ )
+
+ logger.log("Started multi-core benchmark");
+ multiCoreDispatcher.requestRender();
+}
+
+function onMultiCoreBenchmarkComplete(score: number) {
+ multiCoreScore = score;
+ multiCoreScoreElement.textContent = `${multiCoreScore}`;
+ multiCoreScoreElement.classList.remove('loading');
+ logger.log("Started single core benchmark");
+ singleCoreDispatcher.requestRender();
+}
+
+function onSingleCoreBenchmarkComplete(score: number) {
+ singleCoreScore = score;
+ benchmarkButton.classList.remove('loading');
+ singleCoreScoreElement.textContent = `${singleCoreScore}`;
+ singleCoreScoreElement.classList.remove('loading');
+
+ BenchmarkChart.addDeviceResult(multiCoreScore, singleCoreScore);
+}
+
+function initDispatcher(width: number, height: number, options: RaytracerOptions, onComplete: Function): RaytraceDispatcher {
+ const fov = Math.PI / 3;
+ const framebuffer = new Framebuffer(width, height);
+
+ const {spheres, planes, lights} = getScene();
+
+ const context = new RaytraceContext(
+ height,
+ width,
+ fov,
+ spheres,
+ planes,
+ lights,
+ new Colour(221, 221, 221),
+ options
+ );
+
+ return new RaytraceDispatcher(
+ framebuffer,
+ context,
+ logger,
+ onComplete
+ );
+}
+
+function getBenchmarkRenderOptions(multiCore: boolean) {
+ return {
+ numThreads: multiCore ? deviceAvailableThreadCount: 1,
+ shadows: true,
+ diffuseLighting: true,
+ specularLighting: true,
+ reflections: true,
+ refractions: true,
+ maxRecurseDepth: 5,
+ maxDrawDistance: 1000,
+ directMemoryTransfer: true,
+ chunkSize: 0,
+ chunkAllocationMode: ChunkAllocationMode.SEQUENTIAL
+ }
+}
diff --git a/src/ui/demoView.ts b/src/ui/demoView.ts
new file mode 100644
index 0000000..e42696b
--- /dev/null
+++ b/src/ui/demoView.ts
@@ -0,0 +1,141 @@
+import {ChunkAllocationMode, RaytraceContext, RaytracerOptions} from "../models/RaytraceContext";
+import {RaytraceDispatcher} from "../RaytraceDispatcher";
+import {Framebuffer} from "../Framebuffer";
+import {getScene} from "../models/Scene";
+import {Colour} from "../models/Colour";
+import {Logger} from "../Logger";
+
+let dispatcher: RaytraceDispatcher;
+
+const renderButton = document.getElementById('render')!;
+const stopRenderButton = document.getElementById('stop-render')!;
+const viewFullButton = document.getElementById('view-full')!;
+
+export function registerEventListeners() {
+ renderButton.addEventListener('click', render);
+ stopRenderButton.addEventListener('click', stopRender);
+
+ viewFullButton.addEventListener('click', () => {
+ const canvas = document.getElementById(
+ 'render-output'
+ ) as HTMLCanvasElement;
+ window.open()!.document.body.innerHTML = `<img src=${canvas.toDataURL()}>`;
+ });
+
+ const threadsSlider = getInputElement('threads');
+ threadsSlider.addEventListener('input', () => {
+ getInputElement('threads-value').textContent = threadsSlider.value;
+ });
+
+ const threadToggle = getInputElement('enable-threads-toggle');
+ threadToggle.addEventListener('change', () => {
+ threadsSlider.disabled = !threadToggle.checked;
+ });
+}
+
+export function render() {
+ renderButton.classList.add('loading');
+ stopRenderButton.classList.remove('d-hide');
+ viewFullButton.classList.add('d-hide');
+
+ const {width, height} = parseResolution();
+ const options = parseOptions();
+ dispatcher = initDispatcher(width, height, options, onRenderComplete);
+ dispatcher.requestRender();
+}
+
+function stopRender() {
+ dispatcher.stopRender();
+ onRenderComplete();
+}
+
+function onRenderComplete() {
+ renderButton.classList.remove('loading');
+ stopRenderButton.classList.add('d-hide');
+ viewFullButton.classList.remove('d-hide');
+}
+
+function initDispatcher(width: number, height: number, options: RaytracerOptions, onComplete: Function): RaytraceDispatcher {
+ const fov = Math.PI / 3;
+ const framebuffer = new Framebuffer(width, height);
+
+ const {spheres, planes, lights} = getScene();
+
+ const context = new RaytraceContext(
+ height,
+ width,
+ fov,
+ spheres,
+ planes,
+ lights,
+ new Colour(221, 221, 221),
+ options
+ );
+
+ return new RaytraceDispatcher(
+ framebuffer,
+ context,
+ new Logger(document.getElementById('console-demo')!),
+ onComplete
+ );
+}
+
+function parseOptions(): RaytracerOptions {
+ return {
+ numThreads: getDesiredThreadCount(),
+ shadows: getInputElement('shadows-toggle').checked,
+ diffuseLighting: getInputElement('diffuse-toggle').checked,
+ specularLighting: getInputElement('specular-toggle').checked,
+ reflections: getInputElement('reflections-toggle').checked,
+ refractions: getInputElement('refractions-toggle').checked,
+ maxRecurseDepth: 5,
+ maxDrawDistance: 1000,
+ directMemoryTransfer: getInputElement('direct-transfer').checked,
+ chunkSize: parseInt(getInputElement('chunk-size').value, 10),
+ chunkAllocationMode: getChunkAllocationMode()
+ };
+}
+
+function parseResolution(): {width: number; height: number} {
+ switch (getInputElement('res').value) {
+ case '360p':
+ return {width: 640, height: 360};
+ case '480p':
+ return {width: 832, height: 480};
+ case '720p':
+ default:
+ return {width: 1280, height: 720};
+ case '1080p':
+ return {width: 1920, height: 1080};
+ case '1440p':
+ return {width: 2560, height: 1440};
+ case '4k':
+ return {width: 3840, height: 2160};
+ }
+}
+
+function getChunkAllocationMode(): ChunkAllocationMode {
+ switch (getInputElement('chunk-allocation-mode').value) {
+ case 'SEQUENTIAL':
+ default:
+ return ChunkAllocationMode.SEQUENTIAL;
+ case 'RANDOM':
+ return ChunkAllocationMode.RANDOM
+ case 'CENTER_TO_EDGE':
+ return ChunkAllocationMode.CENTER_TO_EDGE
+ case 'EDGE_TO_CENTER':
+ return ChunkAllocationMode.EDGE_TO_CENTER
+ }
+}
+
+function getDesiredThreadCount(): number {
+ if (getInputElement('enable-threads-toggle').checked) {
+ return Number.parseInt(getInputElement('threads').value);
+ } else {
+ return 1;
+ }
+}
+
+function getInputElement(elementId: string) {
+ return document.getElementById(elementId) as HTMLInputElement;
+} \ No newline at end of file
diff --git a/src/ui/index.ts b/src/ui/index.ts
new file mode 100644
index 0000000..683d114
--- /dev/null
+++ b/src/ui/index.ts
@@ -0,0 +1,51 @@
+import * as DemoView from "./demoView";
+import * as BenchmarkView from "./benchmarkView";
+
+let chartInitialised = false;
+
+function registerEventListeners() {
+ document.getElementById('demo-tab')!.addEventListener('click', (ev) => {
+ ev.preventDefault();
+ setActiveTab('demo');
+ });
+
+ document.getElementById('benchmark-tab')!.addEventListener('click', (ev) => {
+ ev.preventDefault();
+ setActiveTab('benchmark');
+ });
+
+ DemoView.registerEventListeners();
+ BenchmarkView.registerEventListeners();
+}
+
+function setActiveTab(tab: string) {
+ if (tab==='demo') {
+ document.getElementById('demo-mode')!.classList.remove('d-hide');
+ document.getElementById('benchmark-mode')!.classList.add('d-hide');
+
+ document.getElementById('demo-tab')!.classList.add('active');
+ document.getElementById('benchmark-tab')!.classList.remove('active');
+ } else {
+ // benchmark
+ document.getElementById('demo-mode')!.classList.add('d-hide');
+ document.getElementById('benchmark-mode')!.classList.remove('d-hide');
+
+ document.getElementById('demo-tab')!.classList.remove('active');
+ document.getElementById('benchmark-tab')!.classList.add('active');
+
+ if (!chartInitialised) {
+ // Init on tab click rather than window load so DOM element has correct size and animations play
+ BenchmarkView.initChart();
+ chartInitialised = true;
+ }
+ }
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ registerEventListeners();
+ if (window.location.hash === '#benchmark') {
+ setActiveTab('benchmark');
+ } else {
+ DemoView.render();
+ }
+});