diff options
Diffstat (limited to 'src/ui')
| -rw-r--r-- | src/ui/benchmarkCharts.ts | 90 | ||||
| -rw-r--r-- | src/ui/benchmarkView.ts | 111 | ||||
| -rw-r--r-- | src/ui/demoView.ts | 141 | ||||
| -rw-r--r-- | src/ui/index.ts | 51 |
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(); + } +}); |