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

Merge branch 'master' into joamag/color

# Conflicts:
#	frontends/sdl/src/main.rs
parents 4f513eed 459f7167
No related branches found
No related tags found
1 merge request!16Support for Game Boy Color (CGB) 😎🖍️
Pipeline #2553 passed
Showing
with 407 additions and 16 deletions
import React, { FC, useEffect, useRef, useState } from "react";
import { ButtonSwitch, Emulator, Info, Pair, PanelTab } from "emukit";
import { GameboyEmulator, SerialDevice, bufferToDataUrl } from "../../../ts";
import "./serial-section.css";
const DEVICE_ICON: { [key: string]: string } = {
null: "🛑",
logger: "📜",
printer: "🖨️"
};
type SerialSectionProps = {
emulator: GameboyEmulator;
style?: string[];
};
export const SerialSection: FC<SerialSectionProps> = ({
emulator,
style = []
}) => {
const classes = () => ["serial-section", ...style].join(" ");
const [loggerData, setLoggerData] = useState<string>();
const [printerImageUrls, setPrinterImageUrls] = useState<string[]>();
const loggerDataRef = useRef<string[]>([]);
const printerDataRef = useRef<string[]>([]);
const loggerRef = useRef<HTMLDivElement>(null);
const imagesRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const onLoggerData = (data: Uint8Array) => {
const byte = data[0];
const charByte = String.fromCharCode(byte);
loggerDataRef.current.push(charByte);
setLoggerData(loggerDataRef.current.join(""));
};
const onPrinterData = (imageBuffer: Uint8Array) => {
const imageUrl = bufferToDataUrl(imageBuffer, 160);
printerDataRef.current.unshift(imageUrl);
setPrinterImageUrls([...printerDataRef.current]);
};
const onLogger = (emulator: Emulator, _params: unknown = {}) => {
const params = _params as Record<string, unknown>;
onLoggerData(params.data as Uint8Array);
};
const onPrinter = (emulator: Emulator, _params: unknown = {}) => {
const params = _params as Record<string, unknown>;
onPrinterData(params.imageBuffer as Uint8Array);
};
emulator.bind("logger", onLogger);
emulator.bind("printer", onPrinter);
return () => {
emulator.unbind("logger", onLogger);
emulator.unbind("printer", onPrinter);
};
}, []);
const onEngineChange = (option: string) => {
emulator.loadSerialDevice(option as SerialDevice);
const optionIcon = DEVICE_ICON[option] ?? "";
emulator.handlers.showToast?.(
`${optionIcon} ${option} attached to the serial port & active`
);
};
const getTabs = () => {
return [
<Info>
<Pair
key="button-device"
name={"Device"}
valueNode={
<ButtonSwitch
options={["null", "logger", "printer"]}
value={emulator.serialDevice}
uppercase={true}
size={"large"}
style={["simple"]}
onChange={onEngineChange}
/>
}
/>
<Pair key="baud-rate" name={"Baud Rate"} value={"1 KB/s"} />
</Info>,
<div className="logger" ref={loggerRef}>
<div className="logger-data">
{loggerData || "Logger contents are empty."}
</div>
</div>,
<div className="printer" ref={imagesRef}>
<div className="printer-lines">
{printerImageUrls ? (
printerImageUrls.map((url, index) => (
<img
key={index}
className="printer-line"
src={url}
/>
))
) : (
<span className="placeholder">
Printer contents are empty.
</span>
)}
</div>
</div>
];
};
const getTabNames = () => {
return ["Settings", "Logger", "Printer"];
};
return (
<div className={classes()}>
<PanelTab
tabs={getTabs()}
tabNames={getTabNames()}
selectors={true}
/>
</div>
);
};
export default SerialSection;
.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;
<svg width="48px" height="48px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-labelledby="swapHorizontalIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="swapHorizontalIconTitle">Swap items (horizontally)</title> <path d="M16 4L19 7L16 10"/> <path d="M4 7L18 7"/> <path d="M7 20L4 17L7 14"/> <path d="M19 17L5 17"/> </svg>
\ No newline at end of file
import {
AudioSpecs,
BenchmarkResult,
SectionInfo,
Compilation,
Compiler,
DebugPanel,
Emulator,
EmulatorBase,
Entry,
......@@ -16,7 +18,13 @@ import {
} from "emukit";
import { PALETTES, PALETTES_MAP } from "./palettes";
import { base64ToBuffer, bufferToBase64 } from "./util";
import { HelpFaqs, HelpKeyboard } from "../react";
import {
DebugAudio,
DebugVideo,
HelpFaqs,
HelpKeyboard,
SerialSection
} from "../react";
import {
Cartridge,
......@@ -79,7 +87,17 @@ const KEYS_NAME: Record<string, number> = {
B: PadKey.B
};
const ROM_PATH = require("../../../res/roms/pocket.gb");
const ROM_PATH = require("../../../res/roms/demo/pocket.gb");
/**
* Enumeration with the values for the complete set of available
* serial devices that can be used in the emulator.
*/
export enum SerialDevice {
Null = "null",
Logger = "logger",
Printer = "printer"
}
/**
* Top level class that controls the emulator behaviour
......@@ -116,6 +134,8 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
private romSize = 0;
private cartridge: Cartridge | null = null;
private _serialDevice: SerialDevice = SerialDevice.Null;
/**
* Associative map for extra settings to be used in
* opaque local storage operations, associated setting
......@@ -389,6 +409,10 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
this.gameBoy.load_boot_default();
const cartridge = this.gameBoy.load_rom_ws(romData);
// in case there's a serial device involved tries to load
// it and initialize for the current Game Boy machine
this.loadSerialDevice();
// updates the name of the currently selected engine
// to the one that has been provided (logic change)
if (engine) this._engine = engine;
......@@ -463,6 +487,18 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
];
}
get sections(): SectionInfo[] {
return [
{
name: "Serial",
icon: require("../res/serial.svg"),
node: SerialSection({
emulator: this
})
}
];
}
get help(): HelpPanel[] {
return [
{
......@@ -476,6 +512,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[] {
return ["neo"];
}
......@@ -485,7 +534,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
}
get romExts(): string[] {
return ["gb"];
return ["gb", "gbc"];
}
get pixelFormat(): PixelFormat {
......@@ -548,6 +597,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
set frequency(value: number) {
value = Math.max(value, 0);
this.logicFrequency = value;
this.gameBoy?.set_clock_freq(value);
this.trigger("frequency", value);
}
......@@ -562,22 +612,22 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
get compiler(): Compiler | null {
if (!this.gameBoy) return null;
return {
name: this.gameBoy.get_compiler(),
version: this.gameBoy.get_compiler_version()
name: this.gameBoy.compiler(),
version: this.gameBoy.compiler_version()
};
}
get compilation(): Compilation | null {
if (!this.gameBoy) return null;
return {
date: this.gameBoy.get_compilation_date(),
time: this.gameBoy.get_compilation_time()
date: this.gameBoy.compilation_date(),
time: this.gameBoy.compilation_time()
};
}
get wasmEngine(): string | null {
if (!this.gameBoy) return null;
return this.gameBoy.get_wasm_engine_ws() ?? null;
return this.gameBoy.wasm_engine_ws() ?? null;
}
get framerate(): number {
......@@ -606,6 +656,18 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
};
}
get audioOutput(): Record<string, number> {
const output = this.gameBoy?.audio_all_output();
if (!output) return {};
return {
master: output[0],
ch1: output[1],
ch2: output[2],
ch3: output[3],
ch4: output[4]
};
}
get palette(): string | undefined {
const paletteObj = PALETTES[this.paletteIndex];
return paletteObj.name;
......@@ -618,6 +680,14 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
this.updatePalette();
}
get serialDevice(): SerialDevice {
return this._serialDevice;
}
set serialDevice(value: SerialDevice) {
this._serialDevice = value;
}
toggleRunning() {
if (this.paused) {
this.resume();
......@@ -660,7 +730,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
}
getVideoState(): boolean {
return this.gameBoy?.get_ppu_enabled() ?? false;
return this.gameBoy?.ppu_enabled() ?? false;
}
pauseAudio() {
......@@ -672,7 +742,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
}
getAudioState(): boolean {
return this.gameBoy?.get_apu_enabled() ?? false;
return this.gameBoy?.apu_enabled() ?? false;
}
getTile(index: number): Uint8Array {
......@@ -712,6 +782,46 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
this.storeSettings();
}
loadSerialDevice(device?: SerialDevice) {
device = device ?? this.serialDevice;
switch (device) {
case SerialDevice.Null:
this.loadNullDevice();
break;
case SerialDevice.Logger:
this.loadLoggerDevice();
break;
case SerialDevice.Printer:
this.loadPrinterDevice();
break;
}
}
loadNullDevice(set = true) {
this.gameBoy?.load_null_ws();
if (set) this.serialDevice = SerialDevice.Null;
}
loadLoggerDevice(set = true) {
this.gameBoy?.load_logger_ws();
if (set) this.serialDevice = SerialDevice.Logger;
}
loadPrinterDevice(set = true) {
this.gameBoy?.load_printer_ws();
if (set) this.serialDevice = SerialDevice.Printer;
}
onLoggerDevice(data: Uint8Array) {
this.trigger("logger", { data: data });
}
onPrinterDevice(imageBuffer: Uint8Array) {
this.trigger("printer", { imageBuffer: imageBuffer });
}
/**
* Tries to load game RAM from the `localStorage` using the
* current cartridge title as the name of the item and
......@@ -780,7 +890,14 @@ export class GameboyEmulator extends EmulatorBase implements Emulator {
declare global {
interface Window {
emulator: GameboyEmulator;
panic: (message: string) => void;
loggerCallback: (data: Uint8Array) => void;
printerCallback: (imageBuffer: Uint8Array) => void;
}
interface Console {
image(url: string, size?: number): void;
}
}
......@@ -788,6 +905,19 @@ window.panic = (message: string) => {
console.error(message);
};
window.loggerCallback = (data: Uint8Array) => {
window.emulator.onLoggerDevice(data);
};
window.printerCallback = (imageBuffer: Uint8Array) => {
window.emulator.onPrinterDevice(imageBuffer);
};
console.image = (url: string, size = 80) => {
const style = `font-size: ${size}px; background-image: url("${url}"); background-size: contain; background-repeat: no-repeat;`;
console.log("%c ", style);
};
const wasm = async () => {
await _wasm();
GameBoy.set_panic_hook_ws();
......
......@@ -15,3 +15,30 @@ export const base64ToBuffer = (base64: string) => {
const buffer = new Uint8Array(array);
return buffer;
};
export const bufferToImageData = (buffer: Uint8Array, width: number) => {
const clampedBuffer = new Uint8ClampedArray(buffer.length);
for (let index = 0; index < clampedBuffer.length; index += 4) {
clampedBuffer[index + 0] = buffer[index];
clampedBuffer[index + 1] = buffer[index + 1];
clampedBuffer[index + 2] = buffer[index + 2];
clampedBuffer[index + 3] = buffer[index + 3];
}
return new ImageData(clampedBuffer, width);
};
export const bufferToDataUrl = (buffer: Uint8Array, width: number) => {
const imageData = bufferToImageData(buffer, width);
const canvas = document.createElement("canvas");
canvas.width = imageData.width;
canvas.height = imageData.height;
const context = canvas.getContext("2d");
context?.putImageData(imageData, 0, 0);
const dataUrl = canvas.toDataURL();
return dataUrl;
};
/*
X-Robots-Tag: all
Access-Control-Allow-Origin: *
File moved
File moved
File moved
File moved
File moved
File moved
User-agent: *
Allow: /
File added
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment