Skip to content
Snippets Groups Projects
Verified Commit f85b3d2d authored by João Magalhães's avatar João Magalhães :rocket:
Browse files

feat: moved debug panel into base emulator

The components have been moved from emukit.
parent b21dcc5a
Branches
Tags
No related merge requests found
Pipeline #2483 passed
Showing with 524 additions and 2 deletions
...@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
* Support for variable clock speed for APU, means variable audio speed * Support for variable clock speed for APU, means variable audio speed
* Moved debug into the base emulator (from emukit)
### Changed ### Changed
......
...@@ -31,6 +31,7 @@ ...@@ -31,6 +31,7 @@
"process": "^0.11.10", "process": "^0.11.10",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"typescript": "^5.0.4" "typescript": "^5.0.4",
"webgl-plot": "^0.7.0"
} }
} }
.audio-gb > .section {
display: inline-block;
vertical-align: top;
}
.audio-gb > .section > .audio-wave {
display: inline-block;
margin-right: 5px;
}
.audio-gb > .section > .audio-wave > h4 {
margin: 4px 0px 4px 0px;
}
import React, { FC, useEffect, useRef, useState } from "react";
import { Canvas, CanvasStructure, PixelFormat } from "emukit";
import { WebglPlot, WebglLine, ColorRGBA } from "webgl-plot";
import "./audio-gb.css";
type AudioGBProps = {
getAudioOutput: () => Record<string, number>;
interval?: number;
drawInterval?: number;
color?: number;
range?: number;
rangeVolume?: number;
engine?: "webgl" | "canvas";
style?: string[];
renderWave?: (name: string, key: string, styles?: string[]) => JSX.Element;
};
export const AudioGB: FC<AudioGBProps> = ({
getAudioOutput,
interval = 1,
drawInterval = 1000 / 60,
color = 0x50cb93ff,
range = 128,
rangeVolume = 32,
engine = "webgl",
style = [],
renderWave
}) => {
const classes = () => ["audio-gb", ...style].join(" ");
const [audioOutput, setAudioOutput] = useState<Record<string, number[]>>(
{}
);
const intervalsRef = useRef<number>();
const intervalsExtraRef = useRef<number>();
useEffect(() => {
const updateAudioOutput = () => {
const _audioOutput = getAudioOutput();
for (const [key, value] of Object.entries(_audioOutput)) {
const values = audioOutput[key] ?? new Array(range).fill(0);
values.push(value);
if (values.length > range) {
values.shift();
}
audioOutput[key] = values;
}
setAudioOutput(audioOutput);
};
setInterval(() => updateAudioOutput(), interval);
updateAudioOutput();
return () => {
if (intervalsRef.current) {
clearInterval(intervalsRef.current);
}
if (intervalsExtraRef.current) {
clearInterval(intervalsExtraRef.current);
}
};
}, []);
const renderAudioWave = (
name: string,
key: string,
styles: string[] = []
) => {
const classes = ["audio-wave", ...styles].join(" ");
const onCanvas = (structure: CanvasStructure) => {
const drawWave = () => {
const values = audioOutput[key];
if (!values) {
return;
}
structure.canvasImage.data.fill(0);
values.forEach((value, index) => {
const valueN = Math.min(value, rangeVolume - 1);
const line = rangeVolume - 1 - valueN;
const offset = (line * range + index) * PixelFormat.RGBA;
structure.canvasBuffer.setUint32(offset, color);
});
structure.canvasOffScreenContext.putImageData(
structure.canvasImage,
0,
0
);
structure.canvasContext.clearRect(0, 0, range, rangeVolume);
structure.canvasContext.drawImage(
structure.canvasOffScreen,
0,
0
);
};
drawWave();
intervalsExtraRef.current = setInterval(
() => drawWave(),
drawInterval
);
};
return (
<div className={classes}>
<h4>{name}</h4>
<Canvas
width={range}
height={rangeVolume}
onCanvas={onCanvas}
/>
</div>
);
};
const renderAudioWaveWgl = (
name: string,
key: string,
styles: string[] = []
) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const classes = ["audio-wave", ...styles].join(" ");
useEffect(() => {
if (!canvasRef.current) return;
// converts the canvas to the expected size according
// to the device pixel ratio value
const devicePixelRatio = window.devicePixelRatio || 1;
canvasRef.current.width =
canvasRef.current.clientWidth * devicePixelRatio;
canvasRef.current.height =
canvasRef.current.clientHeight * devicePixelRatio;
// creates the WGL Plot object with the canvas element
// that is associated with the current audio wave
const wglPlot = new WebglPlot(canvasRef.current);
const colorRgba = new ColorRGBA(...intToColor2(color));
const line = new WebglLine(colorRgba, range);
line.arrangeX();
wglPlot.addLine(line);
const drawWave = () => {
const values = audioOutput[key];
if (!values) {
return;
}
values.forEach((value, index) => {
const valueN = Math.min(value, rangeVolume - 1);
line.setY(index, valueN / rangeVolume - 1);
});
wglPlot.update();
};
drawWave();
intervalsExtraRef.current = setInterval(
() => drawWave(),
drawInterval
);
}, [canvasRef]);
return (
<div className={classes}>
<h4>{name}</h4>
<Canvas
canvasRef={canvasRef}
width={range}
height={rangeVolume}
init={false}
/>
</div>
);
};
let renderMethod =
engine === "webgl" ? renderAudioWaveWgl : renderAudioWave;
renderMethod = renderWave ?? renderMethod;
return (
<div className={classes()}>
<div className="section">
{renderMethod("Master", "master")}
{renderMethod("CH1", "ch1")}
{renderMethod("CH2", "ch2")}
{renderMethod("CH3", "ch3")}
{renderMethod("CH4", "ch4")}
</div>
</div>
);
};
const intToColor = (int: number): [number, number, number, number] => {
const r = (int >> 24) & 0xff;
const g = (int >> 16) & 0xff;
const b = (int >> 8) & 0xff;
const a = int & 0xff;
return [r, g, b, a];
};
const intToColor2 = (int: number): [number, number, number, number] => {
const color = intToColor(int);
return color.map((v) => v / 255) as [number, number, number, number];
};
export default AudioGB;
import React, { FC } from "react";
import { AudioGB } from "../audio-gb/audio-gb";
import { RegistersGB } from "../registers-gb/registers-gb";
import { TilesGB } from "../tiles-gb/tiles-gb";
import { GameboyEmulator } from "../../../ts";
import "./debug.css";
type EmulatorProps = {
emulator: GameboyEmulator;
};
export const DebugVideo: FC<EmulatorProps> = ({ emulator }) => {
return (
<>
{emulator.getTile && (
<div
style={{
display: "inline-block",
verticalAlign: "top",
marginRight: 32,
width: 256
}}
>
<h3>VRAM Tiles</h3>
<TilesGB
getTile={(index) =>
emulator.getTile
? emulator.getTile(index)
: new Uint8Array()
}
tileCount={384}
width={"100%"}
contentBox={false}
/>
</div>
)}
<div
style={{
display: "inline-block",
verticalAlign: "top"
}}
>
<h3>Registers</h3>
<RegistersGB getRegisters={() => emulator.registers} />
</div>
</>
);
};
export const DebugAudio: FC<EmulatorProps> = ({ emulator }) => {
return (
<>
<div
style={{
display: "inline-block",
verticalAlign: "top"
}}
>
<h3>Audio Waveform</h3>
<AudioGB
getAudioOutput={() => emulator.audioOutput}
engine="webgl"
/>
</div>
</>
);
};
export * from "./audio-gb/audio-gb";
export * from "./debug/debug";
export * from "./help/help"; export * from "./help/help";
export * from "./registers-gb/registers-gb";
export * from "./tiles-gb/tiles-gb";
.registers-gb > .section {
display: inline-block;
margin-right: 32px;
vertical-align: top;
}
.registers-gb > .section:last-child {
margin-right: 0px;
}
.registers-gb > .section > h4 {
font-size: 22px;
margin: 0px 0px 8px 0px;
}
.registers-gb > .section > .register {
font-size: 0px;
line-height: 22px;
}
.registers-gb > .section > .register > .register-key {
display: inline-block;
font-size: 20px;
width: 40px;
}
.registers-gb > .section > .register > .register-value {
display: inline-block;
font-size: 20px;
text-align: right;
width: 66px;
}
import React, { FC, useEffect, useRef, useState } from "react";
import "./registers-gb.css";
type RegistersGBProps = {
getRegisters: () => Record<string, string | number>;
interval?: number;
style?: string[];
};
export const RegistersGB: FC<RegistersGBProps> = ({
getRegisters,
interval = 50,
style = []
}) => {
const classes = () => ["registers-gb", ...style].join(" ");
const [registers, setRegisters] = useState<Record<string, string | number>>(
{}
);
const intervalsRef = useRef<number>();
useEffect(() => {
const updateRegisters = () => {
const registers = getRegisters();
setRegisters(registers);
};
setInterval(() => updateRegisters(), interval);
updateRegisters();
return () => {
if (intervalsRef.current) {
clearInterval(intervalsRef.current);
}
};
}, []);
const renderRegister = (
key: string,
value?: number,
size = 2,
styles: string[] = []
) => {
const classes = ["register", ...styles].join(" ");
const valueS =
value?.toString(16).toUpperCase().padStart(size, "0") ?? value;
return (
<div className={classes}>
<span className="register-key">{key}</span>
<span className="register-value">
{valueS ? `0x${valueS}` : "-"}
</span>
</div>
);
};
return (
<div className={classes()}>
<div className="section">
<h4>CPU</h4>
{renderRegister("PC", registers.pc as number, 4)}
{renderRegister("SP", registers.sp as number, 4)}
{renderRegister("A", registers.a as number)}
{renderRegister("B", registers.b as number)}
{renderRegister("C", registers.c as number)}
{renderRegister("D", registers.d as number)}
{renderRegister("E", registers.e as number)}
{renderRegister("H", registers.h as number)}
{renderRegister("L", registers.l as number)}
</div>
<div className="section">
<h4>PPU</h4>
{renderRegister("SCY", registers.scy as number)}
{renderRegister("SCX", registers.scx as number)}
{renderRegister("WY", registers.wy as number)}
{renderRegister("WX", registers.wx as number)}
{renderRegister("LY", registers.ly as number)}
{renderRegister("LYC", registers.lyc as number)}
</div>
</div>
);
};
export default RegistersGB;
.tiles-gb > .canvas {
background-color: #1b1a17;
border: 2px solid #50cb93;
padding: 8px 8px 8px 8px;
}
.tiles-gb > .canvas.content-box {
box-sizing: content-box;
-o-box-sizing: content-box;
-ms-box-sizing: content-box;
-moz-box-sizing: content-box;
-khtml-box-sizing: content-box;
-webkit-box-sizing: content-box;
}
import React, { FC, useEffect, useRef } from "react";
import { Canvas, CanvasStructure, PixelFormat } from "emukit";
import "./tiles-gb.css";
type TilesGBProps = {
getTile: (index: number) => Uint8Array;
tileCount: number;
width?: number | string;
contentBox?: boolean;
interval?: number;
style?: string[];
};
export const TilesGB: FC<TilesGBProps> = ({
getTile,
tileCount,
width,
contentBox = true,
interval = 1000,
style = []
}) => {
const classes = () =>
["tiles-gb", contentBox ? "content-box" : "", ...style].join(" ");
const intervalsRef = useRef<number>();
useEffect(() => {
return () => {
if (intervalsRef.current) {
clearInterval(intervalsRef.current);
}
};
}, []);
const onCanvas = (structure: CanvasStructure) => {
const drawTiles = () => {
for (let index = 0; index < tileCount; index++) {
const pixels = getTile(index);
drawTile(index, pixels, structure);
}
};
drawTiles();
intervalsRef.current = setInterval(() => drawTiles(), interval);
};
return (
<div className={classes()}>
<Canvas
width={128}
height={192}
scale={2}
scaledWidth={width}
onCanvas={onCanvas}
/>
</div>
);
};
/**
* Draws the tile at the given index to the proper vertical
* offset in the given context and buffer.
*
* @param index The index of the sprite to be drawn.
* @param pixels Buffer of pixels that contains the RGB data
* that is going to be drawn.
* @param structure The canvas context to which the tile is
* growing to be drawn.
* @param format The pixel format of the sprite.
*/
const drawTile = (
index: number,
pixels: Uint8Array,
structure: CanvasStructure,
format: PixelFormat = PixelFormat.RGB
) => {
const line = Math.floor(index / 16);
const column = index % 16;
let offset =
(line * structure.canvasOffScreen.width * 8 + column * 8) *
PixelFormat.RGBA;
let counter = 0;
for (let i = 0; i < pixels.length; i += format) {
const color =
(pixels[i] << 24) |
(pixels[i + 1] << 16) |
(pixels[i + 2] << 8) |
(format === PixelFormat.RGBA ? pixels[i + 3] : 0xff);
structure.canvasBuffer.setUint32(offset, color);
counter++;
if (counter === 8) {
counter = 0;
offset += (structure.canvasOffScreen.width - 7) * PixelFormat.RGBA;
} else {
offset += PixelFormat.RGBA;
}
}
structure.canvasOffScreenContext.putImageData(structure.canvasImage, 0, 0);
structure.canvasContext.drawImage(structure.canvasOffScreen, 0, 0);
};
export default TilesGB;
...@@ -3,6 +3,7 @@ import { ...@@ -3,6 +3,7 @@ import {
BenchmarkResult, BenchmarkResult,
Compilation, Compilation,
Compiler, Compiler,
DebugPanel,
Emulator, Emulator,
EmulatorBase, EmulatorBase,
Entry, Entry,
...@@ -16,7 +17,7 @@ import { ...@@ -16,7 +17,7 @@ import {
} from "emukit"; } from "emukit";
import { PALETTES, PALETTES_MAP } from "./palettes"; import { PALETTES, PALETTES_MAP } from "./palettes";
import { base64ToBuffer, bufferToBase64 } from "./util"; import { base64ToBuffer, bufferToBase64 } from "./util";
import { HelpFaqs, HelpKeyboard } from "../react"; import { DebugAudio, DebugVideo, HelpFaqs, HelpKeyboard } from "../react";
import { import {
Cartridge, Cartridge,
...@@ -476,6 +477,19 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { ...@@ -476,6 +477,19 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
]; ];
} }
get debug(): DebugPanel[] {
return [
{
name: "Video",
node: DebugVideo({ emulator: this })
},
{
name: "Audio",
node: DebugAudio({ emulator: this })
}
];
}
get engines(): string[] { get engines(): string[] {
return ["neo"]; return ["neo"];
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment