From 2e0e0d812b76c713d7ac7f94c56358b0597b43c8 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Tue, 14 Oct 2025 11:20:28 +0300 Subject: [PATCH 01/15] devtools: Correctly monitor websocket status --- devtools/src/services/debuggerClient.ts | 10 +++++++++- devtools/src/state/sessionStore.ts | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/devtools/src/services/debuggerClient.ts b/devtools/src/services/debuggerClient.ts index 3c7a499..869a39a 100644 --- a/devtools/src/services/debuggerClient.ts +++ b/devtools/src/services/debuggerClient.ts @@ -11,7 +11,7 @@ import type { import { DebuggerRpcMethodSchemas, RpcMethod } from "../lib/debuggerSchema"; import { JsonRpcClient } from "./jsonRpcClient"; import type { JsonRpcClientOptions } from "./jsonRpcClient"; -import type { DebuggerTransport, TransportOptions } from "./transport"; +import type { DebuggerTransport, TransportOptions, TransportState, TransportStateHandler } from "./transport"; import { createTransport } from "./transport"; export interface DebuggerClientConfig { @@ -137,6 +137,14 @@ export class DebuggerClient { this.notificationHandlers.add(handler); return () => this.notificationHandlers.delete(handler); } + + onTransportStateChange(handler: TransportStateHandler): () => void { + return this.transport.onStateChange(handler); + } + + get transportState(): TransportState { + return this.transport.state; + } } const mapNotification = (notification: JsonRpcNotification): DebuggerNotification | undefined => { diff --git a/devtools/src/state/sessionStore.ts b/devtools/src/state/sessionStore.ts index 2b8f186..2d92530 100644 --- a/devtools/src/state/sessionStore.ts +++ b/devtools/src/state/sessionStore.ts @@ -41,6 +41,29 @@ export const useSessionStore = create()((set, get) => ({ transportOptions, }); + // Subscribe to transport state changes to keep connectionState in sync + client.onTransportStateChange((state, event) => { + console.log("Transport state changed:", state, event); + + if (state === "closed") { + const currentClient = get().client; + // Only update if this is still the active client + if (currentClient === client) { + set({ + connectionState: "error", + connectionError: "Connection closed", + }); + } + } else if (state === "open") { + const currentClient = get().client; + if (currentClient === client) { + set({ + connectionState: "connected", + }); + } + } + }); + set({ connectionState: "connecting", connectionError: undefined, From a864ff9a710d1fd30748ee804c4e4cd5b85f3045 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Tue, 14 Oct 2025 14:46:00 +0300 Subject: [PATCH 02/15] devootls: broadcast channel --- .github/workflows/deploy.yml | 6 +- .vscode/launch.json | 4 +- Cargo.lock | 2 + Cargo.toml | 4 + build.rs | 28 ++- devtools/.gitignore | 3 +- devtools/package-lock.json | 26 +++ devtools/package.json | 10 +- devtools/src/app/layout/AppLayout.tsx | 30 +++- devtools/src/app/layout/ConnectionModal.tsx | 146 ++++++++++++++++ devtools/src/app/layout/HomePage.tsx | 2 +- devtools/src/services/transport.ts | 179 +++++++++++++++++++- devtools/src/state/sessionStore.ts | 82 ++++++++- index.html | 6 +- src/broadcast_debug_server.rs | 153 +++++++++++++++++ src/debugger_server_main.rs | 2 +- 16 files changed, 645 insertions(+), 38 deletions(-) create mode 100644 devtools/src/app/layout/ConnectionModal.tsx create mode 100644 src/broadcast_debug_server.rs diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1ed536e..e002103 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -58,7 +58,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: devtools-dist-${{ matrix.variant }} - path: devtools/dist + path: devtools/dist-${{ matrix.variant }} build-emulator: needs: build-debugger runs-on: ${{ matrix.os }} @@ -143,7 +143,7 @@ jobs: uses: actions/download-artifact@v4 with: name: devtools-dist-native - path: devtools/dist + path: devtools/dist-native - name: "Native: Run SH4 tests" if: matrix.target != 'wasm32-unknown-unknown' && matrix.target != 'aarch64-unknown-linux-gnu' && matrix.target != 'riscv64gc-unknown-linux-gnu' && matrix.target != 'aarch64-pc-windows-msvc' run: cargo test --package sh4-core --target ${{ matrix.target }} @@ -192,7 +192,7 @@ jobs: uses: actions/download-artifact@v4 with: name: devtools-dist-wasm - path: devtools/dist + path: devtools/dist-wasm - name: "WASM: Build code and site" if: matrix.target == 'wasm32-unknown-unknown' diff --git a/.vscode/launch.json b/.vscode/launch.json index 02ba829..73285c0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "name": "(Windows) Launch", "type": "cppvsdbg", "request": "launch", - "program": "${workspaceFolder}/target/debug/nullDC", + "program": "${workspaceFolder}/target/release/nullDC", "args": [], "stopAtEntry": false, "cwd": "C:/Users/skmp/projects/nullDC", @@ -19,7 +19,7 @@ "name": "(Windows) nulldc-minimal", "type": "cppvsdbg", "request": "launch", - "program": "${workspaceFolder}/target/debug/nulldc-minimal", + "program": "${workspaceFolder}/target/release/nulldc-minimal", "args": [], "stopAtEntry": false, "cwd": "C:/Users/skmp/projects/nullDC", diff --git a/Cargo.lock b/Cargo.lock index 4c57e83..351e3f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1831,6 +1831,7 @@ dependencies = [ "futures", "git-version", "include_dir", + "js-sys", "log", "pollster", "serde", @@ -1843,6 +1844,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "wasm-logger", + "web-sys", "wgpu", "winit", ] diff --git a/Cargo.toml b/Cargo.toml index 9d09c42..a424bf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,3 +86,7 @@ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" console_error_panic_hook = "0.1" wasm-logger = "0.2" +web-sys = { version = "0.3", features = ["BroadcastChannel", "MessageEvent", "Window"] } +js-sys = "0.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/build.rs b/build.rs index 12112c5..14599e7 100644 --- a/build.rs +++ b/build.rs @@ -10,18 +10,19 @@ fn main() { println!("cargo:rerun-if-changed=devtools/vite.config.ts"); println!("cargo:rerun-if-changed=devtools/tsconfig.json"); - let dist_path = Path::new("devtools/dist"); + let target_arch = std::env::var("TARGET").unwrap_or_default(); + let is_wasm = target_arch.contains("wasm32"); + + println!( + "cargo:warning=Building devtools for target: {} - is_wasm: {}", + target_arch, is_wasm + ); + + let dist_path = Path::new(if is_wasm { "devtools/dist-wasm" } else { "devtools/dist-native" }); let is_ci = std::env::var("CI").is_ok(); // If not in CI, build the debugger-UI if !is_ci { - let target_arch = std::env::var("TARGET").unwrap_or_default(); - let is_wasm = target_arch.contains("wasm32"); - - println!( - "cargo:warning=Building devtools for target: {}", - target_arch - ); let npm_cmd = if cfg!(target_os = "windows") { "npm.cmd" @@ -40,12 +41,9 @@ fn main() { } let mut build_cmd = Command::new(npm_cmd); - build_cmd.args(&["run", "build"]).current_dir("devtools"); - - // Set environment variable for vite to know if building for wasm - if is_wasm { - build_cmd.env("VITE_USE_BROADCAST", "true"); - } + build_cmd + .args(&["run", if is_wasm { "build:wasm" } else { "build:native" }]) + .current_dir("devtools"); let status = build_cmd.status().expect("Failed to run npm build"); @@ -57,7 +55,7 @@ fn main() { // Verify that dist exists in CI if !dist_path.exists() { - panic!("devtools/dist not found - CI should have provided this artifact"); + panic!("{} not found - CI should have provided this artifact", dist_path.display()); } } } diff --git a/devtools/.gitignore b/devtools/.gitignore index 7bc8784..c058e78 100644 --- a/devtools/.gitignore +++ b/devtools/.gitignore @@ -8,7 +8,8 @@ pnpm-debug.log* lerna-debug.log* node_modules -dist +dist-native +dist-wasm dist-ssr *.local diff --git a/devtools/package-lock.json b/devtools/package-lock.json index 79f0d6b..51d151c 100644 --- a/devtools/package-lock.json +++ b/devtools/package-lock.json @@ -36,6 +36,7 @@ "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^5.0.4", "@vitest/ui": "^3.2.4", + "cross-env": "^10.1.0", "eslint": "^9.36.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.22", @@ -504,6 +505,13 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", @@ -3110,6 +3118,24 @@ "node": ">= 6" } }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", diff --git a/devtools/package.json b/devtools/package.json index 1543aa7..aeb5561 100644 --- a/devtools/package.json +++ b/devtools/package.json @@ -4,11 +4,12 @@ "version": "2.0.0-pre", "type": "module", "scripts": { - "dev": "npm run build:wasm && tsx watch --tsconfig tsconfig.server.json server/mockServer.ts", + "dev": "npm run build:dspsim && tsx watch --tsconfig tsconfig.server.json server/mockServer.ts", "mock:serve": "tsx --tsconfig tsconfig.server.json server/mockServer.ts", - "mock:dev": "npm run build:wasm && tsx --tsconfig tsconfig.server.json server/mockServer.ts", - "build": "npm run build:wasm && tsc -b && vite build", - "build:wasm": "cd crates/aica-dsp && wasm-pack build --target web --out-dir ../../src/wasm/aica-dsp", + "mock:dev": "npm run build:dspsim && tsx --tsconfig tsconfig.server.json server/mockServer.ts", + "build:native": "npm run build:dspsim && tsc -b && vite build --outDir dist-native", + "build:wasm": "npm run build:dspsim && tsc -b && cross-env VITE_TRANSPORT_MODE=wasm vite build --outDir dist-wasm", + "build:dspsim": "cd crates/aica-dsp && wasm-pack build --target web --out-dir ../../src/wasm/aica-dsp", "lint": "eslint .", "test": "vitest", "test:ui": "vitest --ui" @@ -42,6 +43,7 @@ "@types/ws": "^8.18.1", "@vitejs/plugin-react": "^5.0.4", "@vitest/ui": "^3.2.4", + "cross-env": "^10.1.0", "eslint": "^9.36.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.22", diff --git a/devtools/src/app/layout/AppLayout.tsx b/devtools/src/app/layout/AppLayout.tsx index a2cec30..f63a9ac 100644 --- a/devtools/src/app/layout/AppLayout.tsx +++ b/devtools/src/app/layout/AppLayout.tsx @@ -47,6 +47,7 @@ import { TlbContentsPanel, } from "../panels/Sh4CachePanels"; import { AboutDialog } from "./AboutDialog"; +import { ConnectionModal } from "./ConnectionModal"; import { useAboutModal } from "./useAboutModal"; import { TopNav } from "./TopNav"; import { useThemeMode } from "../../theme/ThemeModeProvider"; @@ -153,11 +154,15 @@ interface AppLayoutProps { export const AppLayout = ({ workspaceId }: AppLayoutProps) => { const connect = useSessionStore((state) => state.connect); const disconnect = useSessionStore((state) => state.disconnect); + const startDiscovery = useSessionStore((state) => state.startDiscovery); + const stopDiscovery = useSessionStore((state) => state.stopDiscovery); const connectionState = useSessionStore((state) => state.connectionState); const connectionError = useSessionStore((state) => state.connectionError); const endpoint = useSessionStore((state) => state.endpoint); const client = useSessionStore((state) => state.client); const executionState = useSessionStore((state) => state.executionState); + const showConnectionModal = useSessionStore((state) => state.showConnectionModal); + const setShowConnectionModal = useSessionStore((state) => state.setShowConnectionModal); const initializeData = useDebuggerDataStore((state) => state.initialize); const breakpointHit = useDebuggerDataStore((state) => state.breakpointHit); const errorMessage = useDebuggerDataStore((state) => state.errorMessage); @@ -328,6 +333,15 @@ export const AppLayout = ({ workspaceId }: AppLayoutProps) => { } }, [leftPanelOpen, rightPanelOpen, workspaceId]); + // Start discovery on mount + useEffect(() => { + startDiscovery(); + return () => { + stopDiscovery(); + }; + }, [startDiscovery, stopDiscovery]); + + // Auto-connect once discovery is ready useEffect(() => { void connect(); }, [connect]); @@ -628,11 +642,25 @@ export const AppLayout = ({ workspaceId }: AppLayoutProps) => { > Connection: {connectionState} - Endpoint: {endpoint ?? "-"} + + setShowConnectionModal(true)} + > + Endpoint: {endpoint ?? "-"} + + nullDC Debugger {DEBUGGER_VERSION} + setShowConnectionModal(false)} /> void; +} + +export const ConnectionModal = ({ open, onClose }: ConnectionModalProps) => { + const mode = useSessionStore((state) => state.mode); + const availableConnections = useSessionStore((state) => state.availableConnections); + const selectedConnectionId = useSessionStore((state) => state.selectedConnectionId); + const setSelectedConnectionId = useSessionStore((state) => state.setSelectedConnectionId); + const connect = useSessionStore((state) => state.connect); + + const [manualEntry, setManualEntry] = useState(""); + const [useManual, setUseManual] = useState(false); + + useEffect(() => { + // Reset manual entry when modal opens + if (open) { + setManualEntry(""); + setUseManual(false); + } + }, [open]); + + const handleConnect = async () => { + if (useManual && manualEntry.trim()) { + // Manual connection + await connect({ connectionId: manualEntry.trim(), force: true }); + } else if (selectedConnectionId) { + // Selected from list + await connect({ connectionId: selectedConnectionId, force: true }); + } + onClose(); + }; + + const handleSelectionChange = (id: string) => { + setSelectedConnectionId(id); + setUseManual(false); + }; + + const handleManualClick = () => { + setUseManual(true); + setSelectedConnectionId(undefined); + }; + + const formatLastSeen = (timestamp: number) => { + const seconds = Math.floor((Date.now() - timestamp) / 1000); + if (seconds < 2) return "just now"; + if (seconds < 60) return `${seconds}s ago`; + return `${Math.floor(seconds / 60)}m ago`; + }; + + const canConnect = useManual ? manualEntry.trim().length > 0 : !!selectedConnectionId; + + return ( + + Select Connection + + + {availableConnections.length === 0 && ( + + {mode === "wasm" + ? "No emulator instances detected. Start the emulator and wait for announcements." + : "No WebSocket connections available."} + + )} + + {availableConnections.length > 0 && ( + + + Available Connections + + handleSelectionChange(e.target.value)} + > + {availableConnections.map((conn) => ( + } + label={ + + {conn.name} + + {conn.mode === "wasm" ? `GUID: ${conn.id.substring(0, 8)}...` : conn.id} •{" "} + {formatLastSeen(conn.lastSeen)} + + + } + /> + ))} + + + )} + + + + Manual Connection + + } + label="Enter connection string manually" + /> + {useManual && ( + setManualEntry(e.target.value)} + sx={{ mt: 1 }} + autoFocus + /> + )} + + + + + + + + + ); +}; diff --git a/devtools/src/app/layout/HomePage.tsx b/devtools/src/app/layout/HomePage.tsx index 08db82e..6837040 100644 --- a/devtools/src/app/layout/HomePage.tsx +++ b/devtools/src/app/layout/HomePage.tsx @@ -96,7 +96,7 @@ export const HomePage = () => { Welcome to the nullDC DevTools - Dive into Dreamcast with curated workspaces for CPU, GPU, and audio analysis, as well as sh4 simulator and dsp authoring tools. + Dive into Dreamcast with curated workspaces for CPU, GPU, and audio debugging, as well as sh4 simulator and dsp authoring tools. Choose a task to get started. diff --git a/devtools/src/services/transport.ts b/devtools/src/services/transport.ts index 057a341..8116f7e 100644 --- a/devtools/src/services/transport.ts +++ b/devtools/src/services/transport.ts @@ -8,6 +8,15 @@ export interface TransportOptions { channelName?: string; } +export interface AvailableConnection { + id: string; // GUID for broadcast, URL for websocket + name: string; // Display name + mode: "native" | "wasm"; + lastSeen: number; // Timestamp +} + +export type ConnectionsChangedHandler = (connections: AvailableConnection[]) => void; + export interface DebuggerTransport { readonly kind: "websocket" | "broadcast"; readonly state: TransportState; @@ -110,8 +119,12 @@ export class WebSocketTransport extends BaseTransport { export class BroadcastChannelTransport extends BaseTransport { public readonly kind = "broadcast" as const; private channel?: BroadcastChannel; + private pingInterval?: number; + private pongTimeout?: number; + private readonly PING_INTERVAL_MS = 1000; + private readonly PONG_TIMEOUT_MS = 3000; - async connect(endpoint: string, options?: TransportOptions): Promise { + async connect(endpoint: string, _options?: TransportOptions): Promise { if (typeof BroadcastChannel === "undefined") { throw new Error("BroadcastChannel is not supported in this environment"); } @@ -121,21 +134,38 @@ export class BroadcastChannelTransport extends BaseTransport { } this.state = "connecting"; - const channelName = options?.channelName ?? endpoint; + // endpoint is the GUID for broadcast channel mode + const channelName = `nulldc-debugger-${endpoint}`; const channel = new BroadcastChannel(channelName); this.channel = channel; + channel.addEventListener("message", (event) => { if (typeof event.data === "string") { + // Handle ping/pong messages + if (event.data === "pong") { + if (this.pongTimeout) { + clearTimeout(this.pongTimeout); + this.pongTimeout = undefined; + } + return; + } this.broadcastMessage(event.data); } }); + channel.addEventListener("messageerror", (event) => { this.broadcastState("closed", event); }); + this.broadcastState("open"); + + // Start ping/pong heartbeat + this.startHeartbeat(); } disconnect(): void { + this.stopHeartbeat(); + if (!this.channel) { return; } @@ -150,6 +180,33 @@ export class BroadcastChannelTransport extends BaseTransport { } this.channel.postMessage(payload); } + + private startHeartbeat(): void { + // Send ping every second + this.pingInterval = window.setInterval(() => { + if (this.channel) { + this.channel.postMessage("ping"); + + // Expect pong within 3 seconds + this.pongTimeout = window.setTimeout(() => { + console.warn("Pong timeout - connection lost"); + this.broadcastState("closed"); + this.disconnect(); + }, this.PONG_TIMEOUT_MS); + } + }, this.PING_INTERVAL_MS); + } + + private stopHeartbeat(): void { + if (this.pingInterval) { + clearInterval(this.pingInterval); + this.pingInterval = undefined; + } + if (this.pongTimeout) { + clearTimeout(this.pongTimeout); + this.pongTimeout = undefined; + } + } } export const createTransport = ( @@ -160,3 +217,121 @@ export const createTransport = ( } return new BroadcastChannelTransport(); }; + +// Connection discovery for both websocket and broadcast channel modes +export class ConnectionDiscovery { + private mode: "native" | "wasm"; + private connections = new Map(); + private announcementChannel?: BroadcastChannel; + private cleanupInterval?: number; + private handlers = new Set(); + private readonly EXPIRY_MS = 4000; + private readonly CLEANUP_INTERVAL_MS = 1000; + + constructor(mode: "native" | "wasm") { + this.mode = mode; + } + + start(): void { + if (this.mode === "wasm") { + this.startBroadcastDiscovery(); + } else { + this.startWebSocketDiscovery(); + } + } + + stop(): void { + if (this.announcementChannel) { + this.announcementChannel.close(); + this.announcementChannel = undefined; + } + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = undefined; + } + this.connections.clear(); + } + + getAvailableConnections(): AvailableConnection[] { + return Array.from(this.connections.values()); + } + + onConnectionsChanged(handler: ConnectionsChangedHandler): () => void { + this.handlers.add(handler); + return () => this.handlers.delete(handler); + } + + private startBroadcastDiscovery(): void { + if (typeof BroadcastChannel === "undefined") { + console.warn("BroadcastChannel not supported"); + return; + } + + // Listen for announcements + this.announcementChannel = new BroadcastChannel("nulldc-debugger-announce"); + this.announcementChannel.addEventListener("message", (event) => { + try { + // Handle both JSON string and object formats + let announcement: { id: string; name: string; timestamp: number }; + if (typeof event.data === "string") { + announcement = JSON.parse(event.data); + } else { + announcement = event.data as { id: string; name: string; timestamp: number }; + } + + if (announcement.id && announcement.name) { + const connection: AvailableConnection = { + id: announcement.id, + name: announcement.name, + mode: "wasm", + lastSeen: Date.now(), + }; + const isNew = !this.connections.has(announcement.id); + this.connections.set(announcement.id, connection); + if (isNew) { + this.notifyHandlers(); + } + } + } catch (error) { + console.error("Failed to parse announcement", error); + } + }); + + // Cleanup expired connections + this.cleanupInterval = window.setInterval(() => { + const now = Date.now(); + let changed = false; + for (const [id, conn] of this.connections.entries()) { + if (now - conn.lastSeen > this.EXPIRY_MS) { + this.connections.delete(id); + changed = true; + } + } + if (changed) { + this.notifyHandlers(); + } + }, this.CLEANUP_INTERVAL_MS); + } + + private startWebSocketDiscovery(): void { + // For WebSocket mode, return current host as the only connection + const { protocol, host } = window.location; + const wsProtocol = protocol === "https:" ? "wss:" : "ws:"; + const url = `${wsProtocol}//${host}/ws`; + + const connection: AvailableConnection = { + id: url, + name: `nullDC @ ${host}`, + mode: "native", + lastSeen: Date.now(), + }; + + this.connections.set(url, connection); + this.notifyHandlers(); + } + + private notifyHandlers(): void { + const connections = this.getAvailableConnections(); + this.handlers.forEach((handler) => handler(connections)); + } +} diff --git a/devtools/src/state/sessionStore.ts b/devtools/src/state/sessionStore.ts index 2d92530..1205bd2 100644 --- a/devtools/src/state/sessionStore.ts +++ b/devtools/src/state/sessionStore.ts @@ -1,6 +1,7 @@ import { create } from "zustand"; -import { appConfig, resolveEndpoint, resolveTransportOptions } from "../config"; +import { appConfig } from "../config"; import { DebuggerClient } from "../services/debuggerClient"; +import { ConnectionDiscovery, type AvailableConnection } from "../services/transport"; export type ConnectionState = "idle" | "connecting" | "connected" | "error"; export type ExecutionState = "running" | "paused"; @@ -12,19 +13,55 @@ interface SessionStore { connectionState: ConnectionState; connectionError?: string; executionState: ExecutionState; + connectionDiscovery?: ConnectionDiscovery; + availableConnections: AvailableConnection[]; + selectedConnectionId?: string; + showConnectionModal: boolean; setExecutionState: (state: ExecutionState) => void; - connect: (options?: { force?: boolean }) => Promise; + connect: (options?: { force?: boolean; connectionId?: string }) => Promise; disconnect: () => void; + startDiscovery: () => void; + stopDiscovery: () => void; + setShowConnectionModal: (show: boolean) => void; + setSelectedConnectionId: (id?: string) => void; } export const useSessionStore = create()((set, get) => ({ mode: appConfig.mode, connectionState: "idle", executionState: "running", + availableConnections: [], + showConnectionModal: false, setExecutionState(state) { set({ executionState: state }); }, - async connect({ force } = {}) { + startDiscovery() { + const current = get(); + if (current.connectionDiscovery) { + return; // Already started + } + + const discovery = new ConnectionDiscovery(appConfig.mode); + discovery.onConnectionsChanged((connections) => { + set({ availableConnections: connections }); + }); + discovery.start(); + set({ connectionDiscovery: discovery }); + }, + stopDiscovery() { + const current = get(); + if (current.connectionDiscovery) { + current.connectionDiscovery.stop(); + set({ connectionDiscovery: undefined, availableConnections: [] }); + } + }, + setShowConnectionModal(show) { + set({ showConnectionModal: show }); + }, + setSelectedConnectionId(id) { + set({ selectedConnectionId: id }); + }, + async connect({ force, connectionId } = {}) { const current = get(); if (!force && (current.connectionState === "connecting" || current.connectionState === "connected")) { return; @@ -32,11 +69,42 @@ export const useSessionStore = create()((set, get) => ({ current.client?.disconnect(); - const endpoint = resolveEndpoint(); - const transportOptions = resolveTransportOptions(); + // Determine which connection to use + let targetConnectionId = connectionId || current.selectedConnectionId; + + // If no explicit connection specified, check available connections + if (!targetConnectionId) { + const connections = current.availableConnections; + + if (connections.length === 0) { + // No connections available - show modal + set({ showConnectionModal: true }); + return; + } else if (connections.length === 1) { + // Auto-connect to the only available connection + targetConnectionId = connections[0].id; + } else { + // Multiple connections - show modal for selection + set({ showConnectionModal: true }); + return; + } + } + + // Find the connection details + const connection = current.availableConnections.find((c) => c.id === targetConnectionId); + if (!connection) { + set({ + connectionState: "error", + connectionError: "Selected connection not found", + }); + return; + } + + const endpoint = connection.id; + const transportOptions = connection.mode === "wasm" ? { channelName: endpoint } : undefined; const client = new DebuggerClient({ - mode: appConfig.mode, + mode: connection.mode, endpoint, transportOptions, }); @@ -69,6 +137,8 @@ export const useSessionStore = create()((set, get) => ({ connectionError: undefined, endpoint, client, + selectedConnectionId: targetConnectionId, + showConnectionModal: false, }); try { diff --git a/index.html b/index.html index 79b0b57..4d7a757 100644 --- a/index.html +++ b/index.html @@ -89,7 +89,7 @@

Latest CI/CD downloads

Go to the builds page.

WASM embed

-

Open Debugger for WASM embed | Open Debugger w/ mock server

+

Open Devtools for WASM embed | Open Devtools w/ mock server

@@ -369,7 +369,9 @@

Historical Downloads

- + + + \ No newline at end of file diff --git a/src/broadcast_debug_server.rs b/src/broadcast_debug_server.rs new file mode 100644 index 0000000..d09a4ae --- /dev/null +++ b/src/broadcast_debug_server.rs @@ -0,0 +1,153 @@ +// this module must only ever be compiled for wasm32-unknown-unknown target + +// Broadcast channel debugger server for WASM builds +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; +use web_sys::BroadcastChannel; +use serde::{Deserialize, Serialize}; +use crate::dreamcast::Dreamcast; + +// Generate a simple GUID for this instance using JS Date API +fn generate_guid() -> String { + // Use JavaScript's Date.now() instead of SystemTime (not available in WASM) + let timestamp = js_sys::Date::now() as u64; + let random = (js_sys::Math::random() * 1000000.0) as u64; + format!("{:x}{:x}", timestamp, random) +} + +#[derive(Serialize, Deserialize)] +struct Announcement { + id: String, + name: String, + timestamp: u64, +} + +pub struct BroadcastDebugServer { + guid: String, + announcement_channel: BroadcastChannel, + communication_channel: BroadcastChannel, + announcement_interval: Option, + dreamcast_ptr: usize, +} + +impl BroadcastDebugServer { + pub fn new(dreamcast: *mut Dreamcast) -> Result { + let guid = generate_guid(); + log::info!("Creating broadcast debug server with GUID: {}", guid); + + // Create announcement channel + let announcement_channel = BroadcastChannel::new("nulldc-debugger-announce")?; + + // Create communication channel (per-instance) + let comm_channel_name = format!("nulldc-debugger-{}", guid); + let communication_channel = BroadcastChannel::new(&comm_channel_name)?; + + Ok(Self { + guid, + announcement_channel, + communication_channel, + announcement_interval: None, + dreamcast_ptr: dreamcast as usize, + }) + } + + pub fn start(&mut self) -> Result<(), JsValue> { + log::info!("Starting broadcast debug server"); + + // Start announcement broadcaster + self.start_announcements()?; + + // Setup communication channel message handler + self.setup_communication_handler()?; + + Ok(()) + } + + fn start_announcements(&mut self) -> Result<(), JsValue> { + let guid = self.guid.clone(); + let announcement_channel = self.announcement_channel.clone(); + + let announce_callback = Closure::::new(move || { + let announcement = Announcement { + id: guid.clone(), + name: format!("nullDC Instance {}", &guid[..8]), + timestamp: js_sys::Date::now() as u64, + }; + + if let Ok(json) = serde_json::to_string(&announcement) { + // Post the JSON string directly + let _ = announcement_channel.post_message(&JsValue::from_str(&json)); + } + }); + + // Announce every 1 second + let interval_id = web_sys::window() + .unwrap() + .set_interval_with_callback_and_timeout_and_arguments_0( + announce_callback.as_ref().unchecked_ref(), + 1000, + )?; + + // Store the interval ID so we can clear it later + self.announcement_interval = Some(interval_id); + + // Keep the closure alive + announce_callback.forget(); + + Ok(()) + } + + fn setup_communication_handler(&self) -> Result<(), JsValue> { + let communication_channel = self.communication_channel.clone(); + let _dreamcast_ptr = self.dreamcast_ptr; + + // Handle incoming messages + let message_callback = Closure::::new(move |event: web_sys::MessageEvent| { + let data = event.data(); + + // Check if it's a ping message + if let Some(msg) = data.as_string() { + if msg == "ping" { + // Respond with pong + let _ = communication_channel.post_message(&JsValue::from_str("pong")); + return; + } + + // Handle JSON-RPC messages + // For now, we'll delegate to the mock server logic + // In a full implementation, this would process RPC requests + log::debug!("Received message: {}", msg); + + // TODO: Process JSON-RPC messages using the same handler as WebSocket + // This would call into mock_debug_server::handle_request + } + }); + + self.communication_channel + .set_onmessage(Some(message_callback.as_ref().unchecked_ref())); + + // Keep the closure alive + message_callback.forget(); + + Ok(()) + } + + pub fn stop(&mut self) { + log::info!("Stopping broadcast debug server"); + + // Clear announcement interval + if let Some(interval_id) = self.announcement_interval.take() { + web_sys::window().unwrap().clear_interval_with_handle(interval_id); + } + + // Close channels + self.announcement_channel.close(); + self.communication_channel.close(); + } +} + +impl Drop for BroadcastDebugServer { + fn drop(&mut self) { + self.stop(); + } +} \ No newline at end of file diff --git a/src/debugger_server_main.rs b/src/debugger_server_main.rs index 7ddc20f..7f30487 100644 --- a/src/debugger_server_main.rs +++ b/src/debugger_server_main.rs @@ -8,7 +8,7 @@ use axum::{ use include_dir::{Dir, include_dir}; use nulldc::dreamcast::Dreamcast; -static DEBUGGER_UI: Dir = include_dir!("$CARGO_MANIFEST_DIR/devtools/dist"); +static DEBUGGER_UI: Dir = include_dir!("$CARGO_MANIFEST_DIR/devtools/dist-native"); /// Start the debugger UI HTTP server on port 9999 /// The server runs in a background thread and serves static files From 139f7f777098a00b700ca96afa5c964003a04a64 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Tue, 14 Oct 2025 15:20:09 +0300 Subject: [PATCH 03/15] devtools: cleanups? prob. broken --- Cargo.toml | 2 +- src/broadcast_debug_server.rs | 101 +- src/debugger_core.rs | 1699 +++++++++++++++++++++++++++++++++ src/debugger_server_main.rs | 92 -- src/lib.rs | 29 + src/main.rs | 10 +- src/mock_debug_server.rs | 50 +- src/websocket_debug_server.rs | 153 +++ 8 files changed, 2003 insertions(+), 133 deletions(-) create mode 100644 src/debugger_core.rs delete mode 100644 src/debugger_server_main.rs create mode 100644 src/websocket_debug_server.rs diff --git a/Cargo.toml b/Cargo.toml index a424bf0..962ba5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ bytemuck = "1.16" pollster = "0.4" env_logger = "0.11" log = "0.4" +sha2 = "0.10" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Re-enable native-only features (clipboard, etc.) @@ -78,7 +79,6 @@ tower-http = { version = "0.5", features = ["fs"] } futures = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -sha2 = "0.10" uuid = { version = "1.0", features = ["v4"] } [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/src/broadcast_debug_server.rs b/src/broadcast_debug_server.rs index d09a4ae..d4002d2 100644 --- a/src/broadcast_debug_server.rs +++ b/src/broadcast_debug_server.rs @@ -5,7 +5,9 @@ use wasm_bindgen::prelude::*; use wasm_bindgen::JsValue; use web_sys::BroadcastChannel; use serde::{Deserialize, Serialize}; +use std::sync::Arc; use crate::dreamcast::Dreamcast; +use crate::debugger_core::{JsonRpcRequest, JsonRpcSuccess, JsonRpcError, JsonRpcNotification, ServerState}; // Generate a simple GUID for this instance using JS Date API fn generate_guid() -> String { @@ -28,6 +30,7 @@ pub struct BroadcastDebugServer { communication_channel: BroadcastChannel, announcement_interval: Option, dreamcast_ptr: usize, + state: Arc, } impl BroadcastDebugServer { @@ -42,12 +45,16 @@ impl BroadcastDebugServer { let comm_channel_name = format!("nulldc-debugger-{}", guid); let communication_channel = BroadcastChannel::new(&comm_channel_name)?; + // Create server state + let state = Arc::new(ServerState::new()); + Ok(Self { guid, announcement_channel, communication_channel, announcement_interval: None, dreamcast_ptr: dreamcast as usize, + state, }) } @@ -67,7 +74,18 @@ impl BroadcastDebugServer { let guid = self.guid.clone(); let announcement_channel = self.announcement_channel.clone(); + // Send initial announcement immediately + let announcement = Announcement { + id: guid.clone(), + name: format!("nullDC Instance {}", &guid[..8]), + timestamp: js_sys::Date::now() as u64, + }; + if let Ok(json) = serde_json::to_string(&announcement) { + let _ = announcement_channel.post_message(&JsValue::from_str(&json)); + } + let announce_callback = Closure::::new(move || { + log::debug!("Announcement interval fired"); let announcement = Announcement { id: guid.clone(), name: format!("nullDC Instance {}", &guid[..8]), @@ -75,8 +93,13 @@ impl BroadcastDebugServer { }; if let Ok(json) = serde_json::to_string(&announcement) { + log::debug!("Posting announcement: {}", json); // Post the JSON string directly - let _ = announcement_channel.post_message(&JsValue::from_str(&json)); + if let Err(e) = announcement_channel.post_message(&JsValue::from_str(&json)) { + log::error!("Failed to post announcement: {:?}", e); + } + } else { + log::error!("Failed to serialize announcement"); } }); @@ -99,27 +122,87 @@ impl BroadcastDebugServer { fn setup_communication_handler(&self) -> Result<(), JsValue> { let communication_channel = self.communication_channel.clone(); - let _dreamcast_ptr = self.dreamcast_ptr; + let dreamcast_ptr = self.dreamcast_ptr; + let state = self.state.clone(); // Handle incoming messages let message_callback = Closure::::new(move |event: web_sys::MessageEvent| { + log::debug!("Communication channel received message"); let data = event.data(); // Check if it's a ping message if let Some(msg) = data.as_string() { + log::debug!("Received string message: {}", msg); if msg == "ping" { + log::debug!("Responding to ping with pong"); // Respond with pong - let _ = communication_channel.post_message(&JsValue::from_str("pong")); + if let Err(e) = communication_channel.post_message(&JsValue::from_str("pong")) { + log::error!("Failed to send pong: {:?}", e); + } return; } // Handle JSON-RPC messages - // For now, we'll delegate to the mock server logic - // In a full implementation, this would process RPC requests - log::debug!("Received message: {}", msg); - - // TODO: Process JSON-RPC messages using the same handler as WebSocket - // This would call into mock_debug_server::handle_request + log::debug!("Received JSON-RPC message: {}", msg); + + // Parse and handle the RPC request + if let Ok(request) = serde_json::from_str::(&msg) { + let id = request.id.clone(); + let method = request.method.clone(); + log::debug!("Processing RPC method: {}", method); + + // Call the shared RPC handler + match crate::debugger_core::handle_request(state.clone(), dreamcast_ptr, request) { + Ok((result, should_broadcast)) => { + // Send success response + let response = JsonRpcSuccess { + jsonrpc: "2.0".to_string(), + id, + result, + }; + + if let Ok(response_str) = serde_json::to_string(&response) { + if let Err(e) = communication_channel.post_message(&JsValue::from_str(&response_str)) { + log::error!("Failed to send RPC response: {:?}", e); + } + } + + // Send tick notification if needed + if should_broadcast { + let tick = state.build_tick(dreamcast_ptr, None); + let notification = JsonRpcNotification { + jsonrpc: "2.0".to_string(), + method: "event.tick".to_string(), + params: serde_json::to_value(tick).unwrap(), + }; + + if let Ok(notification_str) = serde_json::to_string(¬ification) { + if let Err(e) = communication_channel.post_message(&JsValue::from_str(¬ification_str)) { + log::error!("Failed to send tick notification: {:?}", e); + } + } + } + } + Err(error) => { + // Send error response + let response = JsonRpcError { + jsonrpc: "2.0".to_string(), + id, + error, + }; + + if let Ok(response_str) = serde_json::to_string(&response) { + if let Err(e) = communication_channel.post_message(&JsValue::from_str(&response_str)) { + log::error!("Failed to send RPC error: {:?}", e); + } + } + } + } + } else { + log::error!("Failed to parse JSON-RPC request: {}", msg); + } + } else { + log::warn!("Received non-string message"); } }); diff --git a/src/debugger_core.rs b/src/debugger_core.rs new file mode 100644 index 0000000..a8b8a12 --- /dev/null +++ b/src/debugger_core.rs @@ -0,0 +1,1699 @@ +// Core debugger logic shared between native (WebSocket) and WASM (BroadcastChannel) builds +// This module contains the complete debugger implementation + +use serde::{Deserialize, Serialize}; +use serde_json::json; +use sha2::{Digest, Sha256}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +#[cfg(not(target_arch = "wasm32"))] +use std::time::{SystemTime, UNIX_EPOCH}; + +const JSON_RPC_VERSION: &str = "2.0"; +const DEFAULT_WATCH_EXPRESSIONS: &[&str] = &["dc.sh4.cpu.pc", "dc.sh4.dmac.dmaor"]; +const EVENT_LOG_LIMIT: usize = 60; +const CAPABILITIES: &[&str] = &["watches", "step", "breakpoints", "frame-log"]; + +#[allow(dead_code)] +mod panel_ids { + pub const DOCUMENTATION: &str = "documentation"; + pub const SH4_SIM: &str = "sh4-sim"; + pub const EVENTS: &str = "events"; + pub const EVENTS_BREAKPOINTS: &str = "events-breakpoints"; + pub const SH4_DISASSEMBLY: &str = "sh4-disassembly"; + pub const SH4_MEMORY: &str = "sh4-memory"; + pub const SH4_BREAKPOINTS: &str = "sh4-breakpoints"; + pub const SH4_BSC_REGISTERS: &str = "bsc-registers"; + pub const SH4_CCN_REGISTERS: &str = "ccn-registers"; + pub const SH4_CPG_REGISTERS: &str = "cpg-registers"; + pub const SH4_DMAC_REGISTERS: &str = "dmac-registers"; + pub const SH4_INTC_REGISTERS: &str = "intc-registers"; + pub const SH4_RTC_REGISTERS: &str = "rtc-registers"; + pub const SH4_SCI_REGISTERS: &str = "sci-registers"; + pub const SH4_SCIF_REGISTERS: &str = "scif-registers"; + pub const SH4_TMU_REGISTERS: &str = "tmu-registers"; + pub const SH4_UBC_REGISTERS: &str = "ubc-registers"; + pub const SH4_SQ_CONTENTS: &str = "sq-contents"; + pub const SH4_ICACHE_CONTENTS: &str = "icache-contents"; + pub const SH4_OCACHE_CONTENTS: &str = "ocache-contents"; + pub const SH4_OCRAM_CONTENTS: &str = "ocram-contents"; + pub const SH4_TLB_CONTENTS: &str = "tlb-contents"; + pub const ARM7_DISASSEMBLY: &str = "arm7-disassembly"; + pub const ARM7_MEMORY: &str = "arm7-memory"; + pub const ARM7_BREAKPOINTS: &str = "arm7-breakpoints"; + pub const CLX2_TA: &str = "holly-ta"; + pub const CLX2_CORE: &str = "holly-core"; + pub const SGC: &str = "sgc"; + pub const DSP_DISASSEMBLY: &str = "dsp-disassembly"; + pub const DSP_BREAKPOINTS: &str = "dsp-breakpoints"; + pub const DSP_PLAYGROUND: &str = "dsp-playground"; + pub const DEVICE_TREE: &str = "device-tree"; + pub const WATCHES: &str = "watches"; + pub const SH4_CALLSTACK: &str = "sh4-callstack"; + pub const ARM7_CALLSTACK: &str = "arm7-callstack"; +} + +type FrameEventGenerator = fn(u64) -> (&'static str, &'static str, String); + +fn frame_event_ta(counter: u64) -> (&'static str, &'static str, String) { + ( + "ta", + "info", + format!("TA/END_LIST tile {}", (counter % 32) as usize), + ) +} + +fn frame_event_core(counter: u64) -> (&'static str, &'static str, String) { + let phase = match counter % 3 { + 0 => "START_RENDER", + 1 => "QUEUE_SUBMISSION", + _ => "END_RENDER", + }; + ( + "core", + if phase == "QUEUE_SUBMISSION" { + "trace" + } else { + "info" + }, + format!("CORE/{}", phase), + ) +} + +fn frame_event_dsp(counter: u64) -> (&'static str, &'static str, String) { + ( + "dsp", + "trace", + format!("DSP/STEP pipeline advanced ({})", counter % 8), + ) +} + +fn frame_event_aica(counter: u64) -> (&'static str, &'static str, String) { + ( + "aica", + "info", + format!("AICA/SGC/STEP channel {}", (counter % 2) as usize), + ) +} + +fn frame_event_sh4(counter: u64) -> (&'static str, &'static str, String) { + ( + "sh4", + "warn", + format!("SH4/INTERRUPT IRQ{} asserted", (counter % 6) + 1), + ) +} + +fn frame_event_holly(counter: u64) -> (&'static str, &'static str, String) { + ( + "holly", + "info", + format!("HOLLY/START_RENDER pass {}", (counter % 4) + 1), + ) +} + +const FRAME_EVENT_GENERATORS: &[FrameEventGenerator] = &[ + frame_event_ta, + frame_event_core, + frame_event_dsp, + frame_event_aica, + frame_event_sh4, + frame_event_holly, +]; + +fn create_frame_event_with_id(event_id: u64) -> EventLogEntry { + let generator = FRAME_EVENT_GENERATORS[(event_id as usize) % FRAME_EVENT_GENERATORS.len()]; + let (subsystem, severity, message) = generator(event_id); + EventLogEntry { + event_id: event_id.to_string(), + timestamp: current_timestamp_ms(), + subsystem: subsystem.to_string(), + severity: severity.to_string(), + message, + metadata: None, + } +} + +fn initial_event_log() -> (Vec, u64) { + let mut log = Vec::new(); + for id in 1..=6 { + log.push(create_frame_event_with_id(id)); + } + (log, 7) +} + +// For WASM, we use js_sys::Date::now() instead of SystemTime +#[cfg(target_arch = "wasm32")] +fn current_timestamp_ms() -> u64 { + js_sys::Date::now() as u64 +} + +#[cfg(not(target_arch = "wasm32"))] +fn current_timestamp_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_else(|_| std::time::Duration::from_millis(0)) + .as_millis() as u64 +} + +// JSON-RPC structures +#[derive(Debug, Serialize, Deserialize)] +pub struct JsonRpcRequest { + pub jsonrpc: String, + pub id: serde_json::Value, + pub method: String, + pub params: Option, +} + +#[derive(Debug, Serialize)] +pub struct JsonRpcSuccess { + pub jsonrpc: String, + pub id: serde_json::Value, + pub result: serde_json::Value, +} + +#[derive(Debug, Serialize)] +pub struct JsonRpcError { + pub jsonrpc: String, + pub id: serde_json::Value, + pub error: JsonRpcErrorObject, +} + +#[derive(Debug, Serialize)] +pub struct JsonRpcErrorObject { + pub code: i32, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +#[derive(Debug, Serialize)] +pub struct JsonRpcNotification { + pub jsonrpc: String, + pub method: String, + pub params: serde_json::Value, +} + +// Debugger schema structures +#[derive(Debug, Clone, Serialize, Deserialize)] +struct RegisterValue { + name: String, + value: String, + width: u32, + #[serde(skip_serializing_if = "Option::is_none")] + flags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + metadata: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct DeviceNodeDescriptor { + path: String, + label: String, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + registers: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + events: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + actions: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + children: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct BreakpointDescriptor { + id: u32, + event: String, + #[serde(skip_serializing_if = "Option::is_none")] + address: Option, + kind: String, // "code" or "event" + enabled: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct CallstackFrame { + index: u32, + pc: u64, + #[serde(skip_serializing_if = "Option::is_none")] + sp: Option, + #[serde(skip_serializing_if = "Option::is_none")] + symbol: Option, + #[serde(skip_serializing_if = "Option::is_none")] + location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct DisassemblyLine { + address: u64, + bytes: String, + disassembly: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct MemorySlice { + #[serde(rename = "baseAddress")] + base_address: u64, + data: Vec, + validity: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct EventLogEntry { + #[serde(rename = "eventId")] + event_id: String, + timestamp: u64, + subsystem: String, + severity: String, + message: String, + #[serde(skip_serializing_if = "Option::is_none")] + metadata: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct WatchDescriptor { + id: u32, + expression: String, + value: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DebuggerTick { + #[serde(rename = "tickId")] + tick_id: u64, + timestamp: u64, + #[serde(rename = "executionState")] + execution_state: ExecutionState, + registers: HashMap>, + breakpoints: HashMap, + #[serde(rename = "eventLog")] + event_log: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + watches: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + callstacks: Option>>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ExecutionState { + state: String, // "running" or "paused" + #[serde(rename = "breakpointId", skip_serializing_if = "Option::is_none")] + breakpoint_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct BreakpointCategoryState { + muted: bool, + soloed: bool, +} + +// Server watch structure +#[derive(Debug, Clone)] +struct ServerWatch { + id: u32, + expression: String, +} + +// Server state +pub struct ServerState { + breakpoints: Arc>>, + watches: Arc>>, + register_values: Arc>>, + event_log: Arc>>, + category_states: Arc>>, + is_running: Arc>, + tick_id: Arc>, + next_event_id: Arc>, + next_watch_id: Arc>, + next_breakpoint_id: Arc>, +} + +impl ServerState { + pub fn new() -> Self { + let mut register_values = HashMap::new(); + + // Initialize register values + register_values.insert("dc.sh4.cpu.pc".to_string(), "0x8C0000A0".to_string()); + register_values.insert("dc.sh4.cpu.pr".to_string(), "0x8C0000A2".to_string()); + register_values.insert("dc.sh4.vbr".to_string(), "0x8C000000".to_string()); + register_values.insert("dc.sh4.sr".to_string(), "0x40000000".to_string()); + register_values.insert("dc.sh4.fpscr".to_string(), "0x00040001".to_string()); + register_values.insert("dc.sh4.cpu.gbr".to_string(), "0x8C000100".to_string()); + register_values.insert("dc.sh4.cpu.mach".to_string(), "0x00000000".to_string()); + register_values.insert("dc.sh4.cpu.macl".to_string(), "0x00000000".to_string()); + register_values.insert("dc.sh4.cpu.fpul".to_string(), "0x00000000".to_string()); + register_values.insert( + "dc.sh4.icache.icache_ctrl".to_string(), + "0x00000003".to_string(), + ); + register_values.insert( + "dc.sh4.dcache.dcache_ctrl".to_string(), + "0x00000003".to_string(), + ); + register_values.insert("dc.sh4.dmac.dmaor".to_string(), "0x8201".to_string()); + register_values.insert("dc.holly.holly_id".to_string(), "0x00050000".to_string()); + register_values.insert("dc.holly.dmac_ctrl".to_string(), "0x00000001".to_string()); + register_values.insert("dc.holly.dmac.dmaor".to_string(), "0x8201".to_string()); + register_values.insert("dc.holly.dmac.chcr0".to_string(), "0x00000001".to_string()); + register_values.insert( + "dc.holly.ta.ta_list_base".to_string(), + "0x0C000000".to_string(), + ); + register_values.insert( + "dc.holly.ta.ta_status".to_string(), + "0x00000000".to_string(), + ); + register_values.insert( + "dc.holly.core.pvr_ctrl".to_string(), + "0x00000001".to_string(), + ); + register_values.insert( + "dc.holly.core.pvr_status".to_string(), + "0x00010000".to_string(), + ); + register_values.insert("dc.aica.aica_ctrl".to_string(), "0x00000002".to_string()); + register_values.insert("dc.aica.aica_status".to_string(), "0x00000001".to_string()); + register_values.insert("dc.aica.arm7.pc".to_string(), "0x00200010".to_string()); + register_values.insert("dc.aica.channels.ch0_vol".to_string(), "0x7F".to_string()); + register_values.insert("dc.aica.channels.ch1_vol".to_string(), "0x6A".to_string()); + register_values.insert("dc.aica.dsp.step".to_string(), "0x000".to_string()); + register_values.insert("dc.aica.dsp.dsp_acc".to_string(), "0x1F".to_string()); + register_values.insert("dc.sysclk".to_string(), "200MHz".to_string()); + register_values.insert("dc.asic_rev".to_string(), "0x0001".to_string()); + + // Initialize default watches + let mut watches = HashMap::new(); + let mut next_watch_id = 1; + for expr in DEFAULT_WATCH_EXPRESSIONS { + watches.insert( + next_watch_id, + ServerWatch { + id: next_watch_id, + expression: expr.to_string(), + }, + ); + next_watch_id += 1; + } + + // Initialize category states + let mut category_states = HashMap::new(); + for category in &["events", "sh4", "arm7", "dsp"] { + category_states.insert( + category.to_string(), + BreakpointCategoryState { + muted: false, + soloed: false, + }, + ); + } + + // Initialize event log with sample entries + let (event_log, next_event_id) = initial_event_log(); + + Self { + breakpoints: Arc::new(Mutex::new(HashMap::new())), + watches: Arc::new(Mutex::new(watches)), + register_values: Arc::new(Mutex::new(register_values)), + event_log: Arc::new(Mutex::new(event_log)), + category_states: Arc::new(Mutex::new(category_states)), + is_running: Arc::new(Mutex::new(true)), + tick_id: Arc::new(Mutex::new(0)), + next_event_id: Arc::new(Mutex::new(next_event_id)), + next_watch_id: Arc::new(Mutex::new(next_watch_id)), + next_breakpoint_id: Arc::new(Mutex::new(1)), + } + } + + fn get_register_value(&self, path: &str, name: &str) -> String { + let key = format!("{}.{}", path, name.to_lowercase()); + self.register_values + .lock() + .unwrap() + .get(&key) + .cloned() + .unwrap_or_else(|| "0x00000000".to_string()) + } + + fn set_register_value(&self, path: &str, name: &str, value: String) { + let key = format!("{}.{}", path, name.to_lowercase()); + self.register_values.lock().unwrap().insert(key, value); + } + + fn evaluate_watch_expression(&self, dreamcast_ptr: usize, expression: &str) -> String { + // Expression format: "dc.sh4.cpu.PC" or just "PC" (defaults to dc.sh4.cpu) + // Split into path and register name + let parts: Vec<&str> = expression.split('.').collect(); + + if parts.is_empty() { + return "0x00000000".to_string(); + } + + // If expression is a full path like "dc.sh4.cpu.R0" + let (path, name) = if parts.len() > 1 { + let name = parts.last().unwrap(); + let path = parts[..parts.len() - 1].join("."); + (path, name.to_string()) + } else { + // Default to dc.sh4.cpu if just register name + ("dc.sh4.cpu".to_string(), parts[0].to_string()) + }; + + // Try to get value from actual emulator if available + #[cfg(not(target_arch = "wasm32"))] + if dreamcast_ptr != 0 { + let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; + if path == "dc.sh4.cpu" { + if let Some(value) = crate::dreamcast::get_sh4_register(dreamcast, &name) { + return format!("0x{:08X}", value); + } + } else if path == "dc.aica.arm7" { + if let Some(value) = crate::dreamcast::get_arm_register(dreamcast, &name) { + return format!("0x{:08X}", value); + } + } + } + + // Fall back to mock values + self.get_register_value(&path, &name) + } + + fn build_device_tree(&self) -> Vec { + let register = |path: &str, name: &str, width: u32| RegisterValue { + name: name.to_string(), + value: self.get_register_value(path, name), + width, + flags: None, + metadata: None, + }; + + let mut sh4_core_registers = vec![ + register("dc.sh4.cpu", "PC", 32), + register("dc.sh4.cpu", "PR", 32), + register("dc.sh4", "VBR", 32), + register("dc.sh4", "SR", 32), + register("dc.sh4", "FPSCR", 32), + register("dc.sh4.cpu", "GBR", 32), + register("dc.sh4.cpu", "MACH", 32), + register("dc.sh4.cpu", "MACL", 32), + register("dc.sh4.cpu", "FPUL", 32), + ]; + for idx in 0..16 { + sh4_core_registers.push(register("dc.sh4.cpu", &format!("R{}", idx), 32)); + } + + let sh4_pbus_children = vec![ + DeviceNodeDescriptor { + path: "dc.sh4.bsc".to_string(), + label: "BSC".to_string(), + description: Some("Bus State Controller".to_string()), + registers: None, + events: None, + actions: Some(vec![panel_ids::SH4_BSC_REGISTERS.to_string()]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.sh4.ccn".to_string(), + label: "CCN".to_string(), + description: Some("Cache Controller".to_string()), + registers: None, + events: None, + actions: Some(vec![panel_ids::SH4_CCN_REGISTERS.to_string()]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.sh4.cpg".to_string(), + label: "CPG".to_string(), + description: Some("Clock Pulse Generator".to_string()), + registers: None, + events: None, + actions: Some(vec![panel_ids::SH4_CPG_REGISTERS.to_string()]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.sh4.dmac".to_string(), + label: "DMAC".to_string(), + description: Some("Direct Memory Access Controller".to_string()), + registers: None, + events: None, + actions: Some(vec![panel_ids::SH4_DMAC_REGISTERS.to_string()]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.sh4.intc".to_string(), + label: "INTC".to_string(), + description: Some("Interrupt Controller".to_string()), + registers: None, + events: None, + actions: Some(vec![panel_ids::SH4_INTC_REGISTERS.to_string()]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.sh4.rtc".to_string(), + label: "RTC".to_string(), + description: Some("Real Time Clock".to_string()), + registers: None, + events: None, + actions: Some(vec![panel_ids::SH4_RTC_REGISTERS.to_string()]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.sh4.sci".to_string(), + label: "SCI".to_string(), + description: Some("Serial Communications Interface".to_string()), + registers: None, + events: None, + actions: Some(vec![panel_ids::SH4_SCI_REGISTERS.to_string()]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.sh4.scif".to_string(), + label: "SCIF".to_string(), + description: Some("Serial Communications Interface w/ FIFO".to_string()), + registers: None, + events: None, + actions: Some(vec![panel_ids::SH4_SCIF_REGISTERS.to_string()]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.sh4.tmu".to_string(), + label: "TMU".to_string(), + description: Some("Timer Unit".to_string()), + registers: None, + events: None, + actions: Some(vec![panel_ids::SH4_TMU_REGISTERS.to_string()]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.sh4.ubc".to_string(), + label: "UBC".to_string(), + description: Some("User Break Controller".to_string()), + registers: None, + events: None, + actions: Some(vec![panel_ids::SH4_UBC_REGISTERS.to_string()]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.sh4.sq".to_string(), + label: "SQ".to_string(), + description: Some("Store Queues".to_string()), + registers: None, + events: None, + actions: Some(vec![panel_ids::SH4_SQ_CONTENTS.to_string()]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.sh4.icache".to_string(), + label: "ICACHE".to_string(), + description: Some("Instruction Cache".to_string()), + registers: None, + events: None, + actions: Some(vec![panel_ids::SH4_ICACHE_CONTENTS.to_string()]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.sh4.ocache".to_string(), + label: "OCACHE".to_string(), + description: Some("Operand Cache".to_string()), + registers: None, + events: None, + actions: Some(vec![panel_ids::SH4_OCACHE_CONTENTS.to_string()]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.sh4.ocram".to_string(), + label: "OCRAM".to_string(), + description: Some("Operand RAM".to_string()), + registers: None, + events: None, + actions: Some(vec![panel_ids::SH4_OCRAM_CONTENTS.to_string()]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.sh4.tlb".to_string(), + label: "TLB".to_string(), + description: Some("Translation Lookaside Buffer".to_string()), + registers: None, + events: None, + actions: Some(vec![panel_ids::SH4_TLB_CONTENTS.to_string()]), + children: None, + }, + ]; + + let sh4_children = vec![ + DeviceNodeDescriptor { + path: "dc.sh4.cpu".to_string(), + label: "Core".to_string(), + description: Some("SuperH4 CPU Core".to_string()), + registers: Some(sh4_core_registers), + events: None, + actions: Some(vec![ + panel_ids::SH4_DISASSEMBLY.to_string(), + panel_ids::SH4_MEMORY.to_string(), + ]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.sh4.pbus".to_string(), + label: "PBUS".to_string(), + description: Some("Peripherals Bus".to_string()), + registers: None, + events: None, + actions: None, + children: Some(sh4_pbus_children), + }, + ]; + + let holly_children = vec![ + DeviceNodeDescriptor { + path: "dc.holly.dmac".to_string(), + label: "DMA Controller".to_string(), + description: Some("peripheral".to_string()), + registers: Some(vec![ + register("dc.holly.dmac", "DMAOR", 16), + register("dc.holly.dmac", "CHCR0", 32), + ]), + events: Some(vec![ + "dc.holly.dmac.transfer_start".to_string(), + "dc.holly.dmac.transfer_end".to_string(), + ]), + actions: None, + children: None, + }, + DeviceNodeDescriptor { + path: "dc.holly.ta".to_string(), + label: "TA".to_string(), + description: Some("Tile Accelerator".to_string()), + registers: Some(vec![ + register("dc.holly.ta", "TA_LIST_BASE", 32), + register("dc.holly.ta", "TA_STATUS", 32), + ]), + events: Some(vec![ + "dc.holly.ta.list_init".to_string(), + "dc.holly.ta.list_end".to_string(), + "dc.holly.ta.opaque_complete".to_string(), + "dc.holly.ta.translucent_complete".to_string(), + ]), + actions: Some(vec![panel_ids::CLX2_TA.to_string()]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.holly.core".to_string(), + label: "CORE".to_string(), + description: Some("Depth and Shading Engine".to_string()), + registers: Some(vec![ + register("dc.holly.core", "PVR_CTRL", 32), + register("dc.holly.core", "PVR_STATUS", 32), + ]), + events: Some(vec![ + "dc.holly.core.render_start".to_string(), + "dc.holly.core.render_end".to_string(), + "dc.holly.core.vblank".to_string(), + ]), + actions: Some(vec![panel_ids::CLX2_CORE.to_string()]), + children: None, + }, + ]; + + let sgc_channels = vec![ + DeviceNodeDescriptor { + path: "dc.aica.sgc.0".to_string(), + label: "Channel 0".to_string(), + description: Some("SGC Channel 0".to_string()), + registers: Some(vec![register("dc.aica.channels", "CH0_VOL", 8)]), + events: Some(vec![ + "dc.aica.channel.0.key_on".to_string(), + "dc.aica.channel.0.key_off".to_string(), + "dc.aica.channel.0.loop".to_string(), + ]), + actions: None, + children: None, + }, + DeviceNodeDescriptor { + path: "dc.aica.sgc.1".to_string(), + label: "Channel 1".to_string(), + description: Some("SGC Channel 1".to_string()), + registers: Some(vec![register("dc.aica.channels", "CH1_VOL", 8)]), + events: Some(vec![ + "dc.aica.channel.0.key_on".to_string(), + "dc.aica.channel.0.key_off".to_string(), + "dc.aica.channel.0.loop".to_string(), + ]), + actions: None, + children: None, + }, + ]; + + let aica_children = vec![ + DeviceNodeDescriptor { + path: "dc.aica.arm7".to_string(), + label: "ARM7".to_string(), + description: Some("ARM7DI CPU Core".to_string()), + registers: Some(vec![register("dc.aica.arm7", "PC", 32)]), + events: None, + actions: Some(vec![ + panel_ids::ARM7_DISASSEMBLY.to_string(), + panel_ids::ARM7_MEMORY.to_string(), + ]), + children: None, + }, + DeviceNodeDescriptor { + path: "dc.aica.sgc".to_string(), + label: "SGC".to_string(), + description: Some("Sound Generation Core".to_string()), + registers: None, + events: Some(vec![ + "dc.aica.channels.key_on".to_string(), + "dc.aica.channels.key_off".to_string(), + "dc.aica.channels.loop".to_string(), + ]), + actions: Some(vec![panel_ids::SGC.to_string()]), + children: Some(sgc_channels), + }, + DeviceNodeDescriptor { + path: "dc.aica.dsp".to_string(), + label: "DSP".to_string(), + description: Some("DSP VLIW Core".to_string()), + registers: Some(vec![ + register("dc.aica.dsp", "STEP", 16), + register("dc.aica.dsp", "DSP_ACC", 16), + ]), + events: Some(vec![ + "dc.aica.dsp.step".to_string(), + "dc.aica.dsp.sample_start".to_string(), + ]), + actions: Some(vec![panel_ids::DSP_DISASSEMBLY.to_string()]), + children: None, + }, + ]; + + vec![DeviceNodeDescriptor { + path: "dc".to_string(), + label: "Dreamcast".to_string(), + description: Some("beloved console".to_string()), + registers: Some(vec![ + register("dc", "SYSCLK", 0), + register("dc", "ASIC_REV", 16), + ]), + events: None, + actions: None, + children: Some(vec![ + DeviceNodeDescriptor { + path: "dc.sh4".to_string(), + label: "SH4".to_string(), + description: Some("SH7750-alike SoC".to_string()), + registers: Some(vec![ + register("dc.sh4", "VBR", 32), + register("dc.sh4", "SR", 32), + register("dc.sh4", "FPSCR", 32), + ]), + events: Some(vec![ + "dc.sh4.interrupt".to_string(), + "dc.sh4.exception".to_string(), + "dc.sh4.tlb_miss".to_string(), + ]), + actions: None, + children: Some(sh4_children), + }, + DeviceNodeDescriptor { + path: "dc.holly".to_string(), + label: "Holly".to_string(), + description: Some("System ASIC".to_string()), + registers: Some(vec![ + register("dc.holly", "HOLLY_ID", 32), + register("dc.holly", "DMAC_CTRL", 32), + ]), + events: None, + actions: None, + children: Some(holly_children), + }, + DeviceNodeDescriptor { + path: "dc.aica".to_string(), + label: "AICA".to_string(), + description: Some("Sound SoC".to_string()), + registers: Some(vec![ + register("dc.aica", "AICA_CTRL", 32), + register("dc.aica", "AICA_STATUS", 32), + ]), + events: Some(vec![ + "dc.aica.interrupt".to_string(), + "dc.aica.timer".to_string(), + ]), + actions: None, + children: Some(aica_children), + }, + ]), + }] + } + + fn set_running(&self, running: bool) { + let mut guard = self.is_running.lock().unwrap(); + *guard = running; + } + + fn increment_program_counter(&self, target: &str) { + let target_lower = target.to_ascii_lowercase(); + if target_lower.contains("sh4") { + if let Some(stripped) = self + .get_register_value("dc.sh4.cpu", "PC") + .strip_prefix("0x") + { + if let Ok(pc) = u32::from_str_radix(stripped, 16) { + let base = 0x8C0000A0; + let offset = pc.wrapping_sub(base); + let new_pc = base + ((offset + 2) % (8 * 2)); + self.set_register_value("dc.sh4.cpu", "PC", format!("0x{:08X}", new_pc)); + } + } + } else if target_lower.contains("arm7") { + if let Some(stripped) = self + .get_register_value("dc.aica.arm7", "PC") + .strip_prefix("0x") + { + if let Ok(pc) = u32::from_str_radix(stripped, 16) { + let base = 0x0020_0010; + let offset = pc.wrapping_sub(base); + let new_pc = base + ((offset + 4) % (8 * 4)); + self.set_register_value("dc.aica.arm7", "PC", format!("0x{:08X}", new_pc)); + } + } + } else if target_lower.contains("dsp") { + if let Some(stripped) = self + .get_register_value("dc.aica.dsp", "STEP") + .strip_prefix("0x") + { + if let Ok(step) = u32::from_str_radix(stripped, 16) { + let new_step = (step + 1) % 8; + self.set_register_value("dc.aica.dsp", "STEP", format!("0x{:03X}", new_step)); + } + } + } + } + + #[allow(dead_code)] + fn next_event_id(&self) -> u64 { + let mut guard = self.next_event_id.lock().unwrap(); + let id = *guard; + *guard += 1; + id + } + + #[allow(dead_code)] + fn push_event(&self, mut entry: EventLogEntry) { + if entry.event_id.is_empty() { + entry.event_id = self.next_event_id().to_string(); + } + entry.timestamp = current_timestamp_ms(); + let mut event_log = self.event_log.lock().unwrap(); + event_log.push(entry); + if event_log.len() > EVENT_LOG_LIMIT { + let remove = event_log.len() - EVENT_LOG_LIMIT; + event_log.drain(0..remove); + } + } + + pub fn build_tick(&self, dreamcast_ptr: usize, hit_breakpoint_id: Option) -> DebuggerTick { + let device_tree = self.build_device_tree(); + let all_registers = collect_registers_from_tree(&device_tree); + + let mut registers_by_id: HashMap> = HashMap::new(); + for (path, registers) in all_registers { + registers_by_id.insert(path, registers); + } + + #[cfg(not(target_arch = "wasm32"))] + if dreamcast_ptr != 0 { + let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; + + let sh4_registers = [ + ("PC", 32), + ("PR", 32), + ("SR", 32), + ("GBR", 32), + ("VBR", 32), + ("MACH", 32), + ("MACL", 32), + ("FPSCR", 32), + ("FPUL", 32), + ]; + let mut sh4_cpu_regs = Vec::new(); + for (name, width) in sh4_registers { + if let Some(value) = crate::dreamcast::get_sh4_register(dreamcast, name) { + sh4_cpu_regs.push(RegisterValue { + name: name.to_string(), + value: format!("0x{:08X}", value), + width, + flags: None, + metadata: None, + }); + } + } + for idx in 0..16 { + let reg_name = format!("R{}", idx); + if let Some(value) = crate::dreamcast::get_sh4_register(dreamcast, ®_name) { + sh4_cpu_regs.push(RegisterValue { + name: reg_name, + value: format!("0x{:08X}", value), + width: 32, + flags: None, + metadata: None, + }); + } + } + if !sh4_cpu_regs.is_empty() { + registers_by_id.insert("dc.sh4.cpu".to_string(), sh4_cpu_regs); + } + + let mut arm_regs = Vec::new(); + if let Some(value) = crate::dreamcast::get_arm_register(dreamcast, "PC") { + arm_regs.push(RegisterValue { + name: "PC".to_string(), + value: format!("0x{:08X}", value), + width: 32, + flags: None, + metadata: None, + }); + } + for idx in 0..16 { + let reg_name = format!("R{}", idx); + if let Some(value) = crate::dreamcast::get_arm_register(dreamcast, ®_name) { + arm_regs.push(RegisterValue { + name: reg_name, + value: format!("0x{:08X}", value), + width: 32, + flags: None, + metadata: None, + }); + } + } + if !arm_regs.is_empty() { + registers_by_id.insert("dc.aica.arm7".to_string(), arm_regs); + } + } + + let breakpoints_by_id = self + .breakpoints + .lock() + .unwrap() + .iter() + .map(|(id, bp)| (id.to_string(), bp.clone())) + .collect::>(); + + let watches = { + let watch_map = self.watches.lock().unwrap(); + if watch_map.is_empty() { + None + } else { + Some( + watch_map + .values() + .map(|watch| WatchDescriptor { + id: watch.id, + expression: watch.expression.clone(), + value: self.evaluate_watch_expression(dreamcast_ptr, &watch.expression), + }) + .collect::>(), + ) + } + }; + + let mut callstacks = HashMap::new(); + + #[cfg(not(target_arch = "wasm32"))] + let sh4_pc = if dreamcast_ptr != 0 { + let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; + crate::dreamcast::get_sh4_register(dreamcast, "PC") + .map(|value| value as u64) + .unwrap_or(0x8C00_00A0) + } else { + self.get_register_value("dc.sh4.cpu", "PC") + .strip_prefix("0x") + .and_then(|value| u64::from_str_radix(value, 16).ok()) + .unwrap_or(0x8C00_00A0) + }; + + #[cfg(target_arch = "wasm32")] + let sh4_pc = self.get_register_value("dc.sh4.cpu", "PC") + .strip_prefix("0x") + .and_then(|value| u64::from_str_radix(value, 16).ok()) + .unwrap_or(0x8C00_00A0); + + let sh4_frames = (0..16) + .map(|index| CallstackFrame { + index, + pc: if index == 0 { + sh4_pc + } else { + 0x8C00_0000 + (index - 1) as u64 * 4 + }, + sp: Some(0x0CFE_0000 - index as u64 * 16), + symbol: Some(format!("SH4_func_{}", index)), + location: Some(format!("sh4.c:{}", 100 + index)), + }) + .collect::>(); + callstacks.insert("sh4".to_string(), sh4_frames); + + #[cfg(not(target_arch = "wasm32"))] + let arm7_pc = if dreamcast_ptr != 0 { + let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; + crate::dreamcast::get_arm_register(dreamcast, "PC") + .map(|value| value as u64) + .unwrap_or(0x0020_0010) + } else { + self.get_register_value("dc.aica.arm7", "PC") + .strip_prefix("0x") + .and_then(|value| u64::from_str_radix(value, 16).ok()) + .unwrap_or(0x0020_0010) + }; + + #[cfg(target_arch = "wasm32")] + let arm7_pc = self.get_register_value("dc.aica.arm7", "PC") + .strip_prefix("0x") + .and_then(|value| u64::from_str_radix(value, 16).ok()) + .unwrap_or(0x0020_0010); + + let arm7_frames = (0..16) + .map(|index| CallstackFrame { + index, + pc: if index == 0 { + arm7_pc + } else { + 0x0020_0000 + (index - 1) as u64 * 4 + }, + sp: Some(0x0028_0000 - index as u64 * 16), + symbol: Some(format!("ARM7_func_{}", index)), + location: Some(format!("arm7.c:{}", 100 + index)), + }) + .collect::>(); + callstacks.insert("arm7".to_string(), arm7_frames); + + let tick_id = { + let mut guard = self.tick_id.lock().unwrap(); + let id = *guard; + *guard += 1; + id + }; + + let timestamp = current_timestamp_ms(); + + #[cfg(not(target_arch = "wasm32"))] + let is_running = if dreamcast_ptr != 0 { + let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; + crate::dreamcast::is_dreamcast_running(dreamcast) + } else { + *self.is_running.lock().unwrap() + }; + + #[cfg(target_arch = "wasm32")] + let is_running = *self.is_running.lock().unwrap(); + + DebuggerTick { + tick_id, + timestamp, + execution_state: ExecutionState { + state: if is_running { + "running".to_string() + } else { + "paused".to_string() + }, + breakpoint_id: hit_breakpoint_id, + }, + registers: registers_by_id, + breakpoints: breakpoints_by_id, + event_log: self.event_log.lock().unwrap().clone(), + watches, + callstacks: Some(callstacks), + } + } +} + +fn sha256_byte(input: &str) -> u8 { + let mut hasher = Sha256::new(); + hasher.update(input.as_bytes()); + let result = hasher.finalize(); + result[0] +} + +fn generate_disassembly(target: &str, address: u64, count: usize) -> Vec { + type OperandFn = fn(u8, u8, u8, u8, u16) -> String; + + let sh4_instructions: Vec<(&str, OperandFn, u64)> = vec![ + ("mov.l", |r1, r2, _, _, _| format!("@r{}+, r{}", r1, r2), 2), + ("mov", |r1, r2, _, _, _| format!("r{}, r{}", r1, r2), 2), + ("sts.l", |r1, _, _, _, _| format!("pr, @-r{}", r1), 2), + ("add", |r1, r2, _, _, _| format!("r{}, r{}", r1, r2), 2), + ("cmp/eq", |r1, r2, _, _, _| format!("r{}, r{}", r1, r2), 2), + ("bf", |_, _, _, _, offset| format!("0x{:x}", offset), 2), + ("jmp", |r, _, _, _, _| format!("@r{}", r), 2), + ("nop", |_, _, _, _, _| String::new(), 2), + ]; + + let arm7_instructions: Vec<(&str, OperandFn, u64)> = vec![ + ("mov", |r1, _, _, val, _| format!("r{}, #{}", r1, val), 4), + ( + "ldr", + |r1, r2, _, _, offset| format!("r{}, [r{}, #{}]", r1, r2, offset), + 4, + ), + ("str", |r1, r2, _, _, _| format!("r{}, [r{}]", r1, r2), 4), + ( + "add", + |r1, r2, r3, _, _| format!("r{}, r{}, r{}", r1, r2, r3), + 4, + ), + ( + "sub", + |r1, r2, r3, _, _| format!("r{}, r{}, r{}", r1, r2, r3), + 4, + ), + ("bx", |r, _, _, _, _| format!("r{}", r), 4), + ("bl", |_, _, _, _, offset| format!("0x{:x}", offset), 4), + ("nop", |_, _, _, _, _| String::new(), 4), + ]; + + let selected = if target == "arm7" { + &arm7_instructions + } else { + &sh4_instructions + }; + + let mut lines = Vec::new(); + let mut current_addr = address; + + for _ in 0..count { + let hash = sha256_byte(&format!("{}:{:x}", target, current_addr)); + let instr_index = (hash as usize) % selected.len(); + let (mnemonic, operand_fn, bytes_len) = selected[instr_index]; + + let r1 = (hash >> 4) % 16; + let r2 = (hash >> 2) % 16; + let r3 = hash % 16; + let val = (hash.wrapping_mul(3)) & 0xff; + let offset = (hash.wrapping_mul(7) as u16) & 0xfff; + + let operands = operand_fn(r1, r2, r3, val, offset); + let disassembly = if operands.is_empty() { + mnemonic.to_string() + } else { + format!("{} {}", mnemonic, operands) + }; + + let byte_values: Vec = (0..bytes_len) + .map(|b| { + format!( + "{:02X}", + sha256_byte(&format!("{}:{:x}:{}", target, current_addr, b)) + ) + }) + .collect(); + + lines.push(DisassemblyLine { + address: current_addr, + bytes: byte_values.join(" "), + disassembly, + }); + + current_addr += bytes_len; + } + + lines +} + +fn build_memory_slice( + dreamcast_ptr: usize, + target: &str, + address: Option, + length: Option, +) -> MemorySlice { + let default_base = match target { + "sh4" => 0x8c000000u64, + "arm7" => 0x00200000u64, + "dsp" => 0x00000000u64, + _ => 0x8c000000u64, + }; + + let base_address = address.unwrap_or(default_base); + let effective_length = length.unwrap_or(64); + + // Try to read from actual emulator memory if pointer is valid + #[cfg(not(target_arch = "wasm32"))] + let bytes: Vec = if dreamcast_ptr != 0 { + let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; + match target { + "arm7" => { + crate::dreamcast::read_arm_memory_slice(dreamcast, base_address, effective_length) + } + _ => crate::dreamcast::read_memory_slice(dreamcast, base_address, effective_length), + } + } else { + // Fall back to mock data if no emulator context + (0..effective_length) + .map(|i| sha256_byte(&format!("{}:{:x}", target, base_address + i as u64))) + .collect() + }; + + #[cfg(target_arch = "wasm32")] + let bytes: Vec = (0..effective_length) + .map(|i| sha256_byte(&format!("{}:{:x}", target, base_address + i as u64))) + .collect(); + + MemorySlice { + base_address, + data: bytes, + validity: "ok".to_string(), + } +} + +fn collect_registers_from_tree(tree: &[DeviceNodeDescriptor]) -> Vec<(String, Vec)> { + let mut result = Vec::new(); + for node in tree { + if let Some(ref registers) = node.registers { + if !registers.is_empty() { + result.push((node.path.clone(), registers.clone())); + } + } + if let Some(ref children) = node.children { + result.extend(collect_registers_from_tree(children)); + } + } + result +} + +pub fn handle_request( + state: Arc, + dreamcast_ptr: usize, + request: JsonRpcRequest, +) -> Result<(serde_json::Value, bool), JsonRpcErrorObject> { + let params = request.params.unwrap_or(json!({})); + + match request.method.as_str() { + "debugger.describe" => { + let device_tree = state.build_device_tree(); + Ok(( + json!({ + "emulator": { + "name": "nullDC", + "version": "unspecified", + "build": if cfg!(target_arch = "wasm32") { "wasm" } else { "native" } + }, + "deviceTree": device_tree, + "capabilities": CAPABILITIES, + }), + true, + )) + } + + "state.getMemorySlice" => { + let target = params + .get("target") + .and_then(|value| value.as_str()) + .unwrap_or("sh4"); + let address = params.get("address").and_then(|value| value.as_u64()); + let length = params + .get("length") + .and_then(|value| value.as_u64()) + .map(|value| value as usize); + let slice = build_memory_slice(dreamcast_ptr, target, address, length); + Ok((serde_json::to_value(slice).unwrap(), false)) + } + + "state.getDisassembly" => { + let target = params + .get("target") + .and_then(|value| value.as_str()) + .unwrap_or("sh4"); + let address = params + .get("address") + .and_then(|value| value.as_u64()) + .unwrap_or(0); + let count = params + .get("count") + .and_then(|value| value.as_u64()) + .unwrap_or(128) as usize; + + #[cfg(not(target_arch = "wasm32"))] + let lines = if dreamcast_ptr != 0 { + let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; + match target { + "sh4" => crate::dreamcast::disassemble_sh4(dreamcast, address, count) + .into_iter() + .map(|line| DisassemblyLine { + address: line.address, + bytes: line.bytes, + disassembly: line.disassembly, + }) + .collect::>(), + "arm7" => crate::dreamcast::disassemble_arm7(dreamcast, address, count) + .into_iter() + .map(|line| DisassemblyLine { + address: line.address, + bytes: line.bytes, + disassembly: line.disassembly, + }) + .collect::>(), + _ => generate_disassembly(target, address, count), + } + } else { + generate_disassembly(target, address, count) + }; + + #[cfg(target_arch = "wasm32")] + let lines = generate_disassembly(target, address, count); + + Ok((json!({ "lines": lines }), false)) + } + + "state.getCallstack" => { + let target = params + .get("target") + .and_then(|value| value.as_str()) + .unwrap_or("sh4"); + let max_frames = params + .get("maxFrames") + .and_then(|value| value.as_u64()) + .unwrap_or(16) + .min(64) as usize; + + let frames: Vec = (0..max_frames) + .map(|index| CallstackFrame { + index: index as u32, + pc: 0x8c000000 + index as u64 * 4, + sp: Some(0x0cfe0000 - index as u64 * 16), + symbol: Some(format!("{}_func_{}", target.to_uppercase(), index)), + location: Some(format!("{}.c:{}", target, 100 + index)), + }) + .collect(); + + Ok((json!({ "target": target, "frames": frames }), false)) + } + + "state.watch" => { + let expressions = params + .get("expressions") + .and_then(|value| value.as_array()) + .cloned() + .unwrap_or_default() + .into_iter() + .filter_map(|value| value.as_str().map(|s| s.to_owned())) + .collect::>(); + + let mut watches = state.watches.lock().unwrap(); + let mut next_id = state.next_watch_id.lock().unwrap(); + + for expr in expressions { + let id = *next_id; + watches.insert( + id, + ServerWatch { + id, + expression: expr, + }, + ); + *next_id += 1; + } + + Ok((json!({}), true)) + } + + "state.unwatch" => { + let watch_ids = params + .get("watchIds") + .and_then(|value| value.as_array()) + .cloned() + .unwrap_or_default() + .into_iter() + .filter_map(|value| value.as_u64()) + .map(|value| value as u32) + .collect::>(); + + let mut watches = state.watches.lock().unwrap(); + for id in watch_ids { + watches.remove(&id); + } + + Ok((json!({}), true)) + } + + "state.editWatch" => { + let watch_id = params + .get("watchId") + .and_then(|value| value.as_u64()) + .map(|value| value as u32); + let value = params + .get("value") + .and_then(|value| value.as_str()) + .unwrap_or(""); + + if let Some(id) = watch_id { + let expression = { + let watches = state.watches.lock().unwrap(); + watches.get(&id).map(|watch| watch.expression.clone()) + }; + + if let Some(expr) = expression { + let parts: Vec<&str> = expr.split('.').collect(); + let (path, name) = if parts.len() > 1 { + let name = parts.last().unwrap(); + let path = parts[..parts.len() - 1].join("."); + (path, name.to_string()) + } else { + ("dc.sh4.cpu".to_string(), parts[0].to_string()) + }; + let key = format!("{}.{}", path, name.to_lowercase()); + + { + let registers = state.register_values.lock().unwrap(); + if !registers.contains_key(&key) { + return Ok(( + json!({ + "error": { + "code": -32602, + "message": format!( + "Cannot edit non-register expression \"{}\"", + expr + ), + } + }), + false, + )); + } + } + + state.set_register_value(&path, &name, value.to_string()); + return Ok((json!({}), true)); + } + + return Ok(( + json!({ + "error": { + "code": -32602, + "message": format!("Watch \"{}\" not found", id), + } + }), + false, + )); + } + + Ok(( + json!({ + "error": { + "code": -32602, + "message": "Watch not found or cannot edit", + } + }), + false, + )) + } + + "state.modifyWatchExpression" => { + let watch_id = params + .get("watchId") + .and_then(|value| value.as_u64()) + .map(|value| value as u32); + let new_expression = params + .get("newExpression") + .and_then(|value| value.as_str()) + .unwrap_or(""); + + if let Some(id) = watch_id { + let mut watches = state.watches.lock().unwrap(); + if let Some(watch) = watches.get_mut(&id) { + watch.expression = new_expression.to_string(); + return Ok((json!({}), true)); + } + + return Ok(( + json!({ + "error": { + "code": -32602, + "message": format!("Watch {} not found", id), + } + }), + false, + )); + } + + Ok(( + json!({ + "error": { + "code": -32602, + "message": "Watch not found", + } + }), + false, + )) + } + + "control.pause" => { + #[cfg(not(target_arch = "wasm32"))] + if dreamcast_ptr != 0 { + let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; + crate::dreamcast::set_dreamcast_running(dreamcast, false); + } + state.set_running(false); + Ok((json!({}), true)) + } + + "control.step" | "control.stepOver" | "control.stepOut" => { + let target = params + .get("target") + .and_then(|value| value.as_str()) + .unwrap_or("sh4"); + + #[cfg(not(target_arch = "wasm32"))] + if dreamcast_ptr != 0 { + let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; + crate::dreamcast::step_dreamcast(dreamcast); + crate::dreamcast::set_dreamcast_running(dreamcast, false); + } else { + state.increment_program_counter(target); + } + + #[cfg(target_arch = "wasm32")] + state.increment_program_counter(target); + + state.set_running(false); + Ok((json!({}), true)) + } + + "control.runUntil" => { + #[cfg(not(target_arch = "wasm32"))] + if dreamcast_ptr != 0 { + let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; + crate::dreamcast::set_dreamcast_running(dreamcast, true); + } + state.set_running(true); + Ok((json!({}), true)) + } + + "breakpoints.add" => { + let event = params + .get("event") + .and_then(|value| value.as_str()) + .unwrap_or(""); + let address = params.get("address").and_then(|value| value.as_u64()); + let kind = params + .get("kind") + .and_then(|value| value.as_str()) + .unwrap_or("code"); + let enabled = params + .get("enabled") + .and_then(|value| value.as_bool()) + .unwrap_or(true); + + let mut next_id = state.next_breakpoint_id.lock().unwrap(); + let id = *next_id; + *next_id += 1; + + let breakpoint = BreakpointDescriptor { + id, + event: event.to_string(), + address, + kind: kind.to_string(), + enabled, + }; + + state.breakpoints.lock().unwrap().insert(id, breakpoint); + Ok((json!({}), true)) + } + + "breakpoints.remove" => { + if let Some(id) = params + .get("id") + .and_then(|value| value.as_u64()) + .map(|value| value as u32) + { + let removed = state.breakpoints.lock().unwrap().remove(&id).is_some(); + if removed { + return Ok((json!({}), true)); + } + + return Ok(( + json!({ + "error": { + "code": -32000, + "message": format!("Breakpoint {} not found", id), + } + }), + false, + )); + } + + Ok((json!({}), false)) + } + + "breakpoints.toggle" => { + let id = params + .get("id") + .and_then(|value| value.as_u64()) + .map(|value| value as u32); + let enabled = params + .get("enabled") + .and_then(|value| value.as_bool()) + .unwrap_or(true); + + if let Some(id) = id { + let mut breakpoints = state.breakpoints.lock().unwrap(); + if let Some(bp) = breakpoints.get_mut(&id) { + bp.enabled = enabled; + return Ok((json!({}), true)); + } + + return Ok(( + json!({ + "error": { + "code": -32000, + "message": format!("Breakpoint {} not found", id), + } + }), + false, + )); + } + + Ok((json!({}), false)) + } + + "breakpoints.setCategoryStates" => { + if let Some(categories) = params.get("categories").and_then(|value| value.as_object()) { + let mut category_states = state.category_states.lock().unwrap(); + for (category, state_value) in categories { + if let (Some(muted), Some(soloed)) = ( + state_value.get("muted").and_then(|value| value.as_bool()), + state_value.get("soloed").and_then(|value| value.as_bool()), + ) { + category_states + .insert(category.clone(), BreakpointCategoryState { muted, soloed }); + } + } + } + Ok((json!({}), true)) + } + + _ => Err(JsonRpcErrorObject { + code: -32601, + message: format!("Method not found: {}", request.method), + data: None, + }), + } +} diff --git a/src/debugger_server_main.rs b/src/debugger_server_main.rs deleted file mode 100644 index 7f30487..0000000 --- a/src/debugger_server_main.rs +++ /dev/null @@ -1,92 +0,0 @@ -use axum::{ - Router, - extract::ws::{WebSocket, WebSocketUpgrade}, - http::{StatusCode, header}, - response::{IntoResponse, Response}, - routing::get, -}; -use include_dir::{Dir, include_dir}; -use nulldc::dreamcast::Dreamcast; - -static DEBUGGER_UI: Dir = include_dir!("$CARGO_MANIFEST_DIR/devtools/dist-native"); - -/// Start the debugger UI HTTP server on port 9999 -/// The server runs in a background thread and serves static files -/// Also handles WebSocket connections for the debugger protocol -pub fn start_debugger_server(dreamcast: *mut Dreamcast) { - use std::thread; - - let dc_ptr = dreamcast as usize; - - thread::spawn(move || { - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(async move { - let app = Router::new() - .route( - "/ws", - get(move |ws: WebSocketUpgrade| websocket_handler(ws, dc_ptr)), - ) - .fallback(static_file_handler); - - let listener = tokio::net::TcpListener::bind("127.0.0.1:55543") - .await - .unwrap(); - - println!( - "Debugger UI server started at http://{}", - listener.local_addr().unwrap() - ); - - axum::serve(listener, app).await.unwrap(); - }); - }); -} - -async fn websocket_handler(ws: WebSocketUpgrade, dc_ptr: usize) -> impl IntoResponse { - ws.on_upgrade(move |socket| handle_websocket(socket, dc_ptr)) -} - -async fn handle_websocket(socket: WebSocket, dc_ptr: usize) { - crate::mock_debug_server::handle_websocket_connection(socket, dc_ptr).await; -} - -async fn static_file_handler(uri: axum::http::Uri) -> Response { - let path = uri.path(); - - // If path does not start with /assets/, always return index.html - let file_path = if path == "/" || !path.starts_with("/assets/") { - "index.html" - } else { - // Strip the leading slash for assets - &path[1..] - }; - - // Try to find the file in the embedded directory - if let Some(file) = DEBUGGER_UI.get_file(file_path) { - let content_type = match file_path.split('.').last() { - Some("html") => "text/html", - Some("js") => "application/javascript", - Some("css") => "text/css", - Some("png") => "image/png", - Some("jpg") | Some("jpeg") => "image/jpeg", - Some("svg") => "image/svg+xml", - Some("wasm") => "application/wasm", - _ => "application/octet-stream", - }; - - Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, content_type) - .header(header::CACHE_CONTROL, "no-cache") - .body(axum::body::Body::from(file.contents())) - .unwrap() - } else { - (StatusCode::NOT_FOUND, "404 Not Found").into_response() - } -} - -/// No-op for WASM builds (debugger uses BroadcastChannel instead) -#[cfg(target_arch = "wasm32")] -pub fn start_debugger_server() { - // No HTTP server needed for WASM - uses BroadcastChannel -} diff --git a/src/lib.rs b/src/lib.rs index 6f3fbb8..d19c0b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,18 @@ use wgpu::util::DeviceExt; pub use dreamcast; use dreamcast::{Dreamcast, present_for_texture, run_slice_dreamcast}; +mod debugger_core; + +#[cfg(target_arch = "wasm32")] +mod broadcast_debug_server; +#[cfg(target_arch = "wasm32")] +use broadcast_debug_server::BroadcastDebugServer; + +#[cfg(not(target_arch = "wasm32"))] +mod websocket_debug_server; +#[cfg(not(target_arch = "wasm32"))] +pub use websocket_debug_server::start_debugger_server; + const GIT_HASH: &str = git_version!(); struct State { @@ -715,6 +727,23 @@ pub async fn wasm_main_with_bios(bios_rom: Vec, bios_flash: Vec) { // Create and initialize Dreamcast with provided BIOS let dc = Box::into_raw(Box::new(Dreamcast::default())); dreamcast::init_dreamcast(dc, &bios_rom, &bios_flash); + let debug_server = BroadcastDebugServer::new(dc); + + // Start broadcast debug server for WASM + match debug_server { + Ok(mut server) => { + if let Err(e) = server.start() { + log::error!("Failed to start broadcast debug server: {:?}", e); + } else { + log::info!("Broadcast debug server started successfully"); + // Keep the server alive by forgetting it (leak it intentionally) + std::mem::forget(server); + } + } + Err(e) => { + log::error!("Failed to create broadcast debug server: {:?}", e); + } + } run(Some(dc)).await; } diff --git a/src/main.rs b/src/main.rs index 2799751..2a32e07 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,7 @@ -#[cfg(not(target_arch = "wasm32"))] -mod debugger_server_main; -#[cfg(not(target_arch = "wasm32"))] -mod mock_debug_server; - use nulldc::dreamcast::{Dreamcast, init_dreamcast}; + +#[cfg(not(target_arch = "wasm32"))] +use nulldc::start_debugger_server; use std::fs; fn load_bios_files() -> (Vec, Vec) { @@ -32,7 +30,7 @@ fn main() { init_dreamcast(dreamcast, &bios_rom, &bios_flash); #[cfg(not(target_arch = "wasm32"))] - debugger_server_main::start_debugger_server(dreamcast); + start_debugger_server(dreamcast); pollster::block_on(nulldc::run(Some(dreamcast))); } diff --git a/src/mock_debug_server.rs b/src/mock_debug_server.rs index 345bbec..6f0adfe 100644 --- a/src/mock_debug_server.rs +++ b/src/mock_debug_server.rs @@ -151,40 +151,40 @@ fn current_timestamp_ms() -> u64 { // JSON-RPC structures #[derive(Debug, Serialize, Deserialize)] -struct JsonRpcRequest { - jsonrpc: String, - id: serde_json::Value, - method: String, - params: Option, +pub struct JsonRpcRequest { + pub jsonrpc: String, + pub id: serde_json::Value, + pub method: String, + pub params: Option, } #[derive(Debug, Serialize)] -struct JsonRpcSuccess { - jsonrpc: String, - id: serde_json::Value, - result: serde_json::Value, +pub struct JsonRpcSuccess { + pub jsonrpc: String, + pub id: serde_json::Value, + pub result: serde_json::Value, } #[derive(Debug, Serialize)] -struct JsonRpcError { - jsonrpc: String, - id: serde_json::Value, - error: JsonRpcErrorObject, +pub struct JsonRpcError { + pub jsonrpc: String, + pub id: serde_json::Value, + pub error: JsonRpcErrorObject, } #[derive(Debug, Serialize)] -struct JsonRpcErrorObject { - code: i32, - message: String, +pub struct JsonRpcErrorObject { + pub code: i32, + pub message: String, #[serde(skip_serializing_if = "Option::is_none")] - data: Option, + pub data: Option, } #[derive(Debug, Serialize)] -struct JsonRpcNotification { - jsonrpc: String, - method: String, - params: serde_json::Value, +pub struct JsonRpcNotification { + pub jsonrpc: String, + pub method: String, + pub params: serde_json::Value, } // Debugger schema structures @@ -309,7 +309,7 @@ struct ServerWatch { } // Server state -struct ServerState { +pub struct ServerState { breakpoints: Arc>>, watches: Arc>>, register_values: Arc>>, @@ -323,7 +323,7 @@ struct ServerState { } impl ServerState { - fn new() -> Self { + pub fn new() -> Self { let mut register_values = HashMap::new(); // Initialize register values @@ -904,7 +904,7 @@ impl ServerState { } } - fn build_tick(&self, dreamcast_ptr: usize, hit_breakpoint_id: Option) -> DebuggerTick { + pub fn build_tick(&self, dreamcast_ptr: usize, hit_breakpoint_id: Option) -> DebuggerTick { let device_tree = self.build_device_tree(); let all_registers = collect_registers_from_tree(&device_tree); @@ -1244,7 +1244,7 @@ fn collect_registers_from_tree(tree: &[DeviceNodeDescriptor]) -> Vec<(String, Ve result } -fn handle_request( +pub fn handle_request( state: Arc, dreamcast_ptr: usize, request: JsonRpcRequest, diff --git a/src/websocket_debug_server.rs b/src/websocket_debug_server.rs new file mode 100644 index 0000000..4840082 --- /dev/null +++ b/src/websocket_debug_server.rs @@ -0,0 +1,153 @@ +use axum::{ + Router, + extract::ws::{WebSocket, WebSocketUpgrade, Message}, + http::{StatusCode, header}, + response::{IntoResponse, Response}, + routing::get, +}; +use futures::SinkExt; +use futures::stream::StreamExt; +use include_dir::{Dir, include_dir}; +use crate::dreamcast::Dreamcast; +use std::sync::Arc; + +use crate::debugger_core::{ + handle_request, JsonRpcError, JsonRpcNotification, JsonRpcRequest, JsonRpcSuccess, + ServerState, +}; + +static DEBUGGER_UI: Dir = include_dir!("$CARGO_MANIFEST_DIR/devtools/dist-native"); + +const JSON_RPC_VERSION: &str = "2.0"; + +/// Start the debugger UI HTTP server on port 55543 +/// The server runs in a background thread and serves static files +/// Also handles WebSocket connections for the debugger protocol +pub fn start_debugger_server(dreamcast: *mut Dreamcast) { + use std::thread; + + let dc_ptr = dreamcast as usize; + + thread::spawn(move || { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(async move { + let app = Router::new() + .route( + "/ws", + get(move |ws: WebSocketUpgrade| websocket_handler(ws, dc_ptr)), + ) + .fallback(static_file_handler); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:55543") + .await + .unwrap(); + + println!( + "Debugger UI server started at http://{}", + listener.local_addr().unwrap() + ); + + axum::serve(listener, app).await.unwrap(); + }); + }); +} + +async fn websocket_handler(ws: WebSocketUpgrade, dc_ptr: usize) -> impl IntoResponse { + ws.on_upgrade(move |socket| handle_websocket(socket, dc_ptr)) +} + +async fn handle_websocket(socket: WebSocket, dreamcast_ptr: usize) { + use std::sync::OnceLock; + static STATE: OnceLock> = OnceLock::new(); + let state = STATE.get_or_init(|| Arc::new(ServerState::new())).clone(); + + let (mut sender, mut receiver) = socket.split(); + + while let Some(Ok(msg)) = receiver.next().await { + match msg { + Message::Text(text) => { + if let Ok(request) = serde_json::from_str::(&text) { + let id = request.id.clone(); + match handle_request(state.clone(), dreamcast_ptr, request) { + Ok((result, should_broadcast)) => { + let response = JsonRpcSuccess { + jsonrpc: JSON_RPC_VERSION.to_string(), + id, + result, + }; + if let Ok(json) = serde_json::to_string(&response) { + let _ = sender.send(Message::Text(json.into())).await; + } + + if should_broadcast { + let tick = state.build_tick(dreamcast_ptr, None); + let notification = JsonRpcNotification { + jsonrpc: JSON_RPC_VERSION.to_string(), + method: "event.tick".to_string(), + params: serde_json::to_value(tick).unwrap(), + }; + + if let Ok(json) = serde_json::to_string(¬ification) { + let _ = sender.send(Message::Text(json.into())).await; + } + } + } + Err(error) => { + let response = JsonRpcError { + jsonrpc: JSON_RPC_VERSION.to_string(), + id, + error, + }; + if let Ok(json) = serde_json::to_string(&response) { + let _ = sender.send(Message::Text(json.into())).await; + } + } + } + } + } + Message::Close(_) => break, + _ => {} + } + } +} + +async fn static_file_handler(uri: axum::http::Uri) -> Response { + let path = uri.path(); + + // If path does not start with /assets/, always return index.html + let file_path = if path == "/" || !path.starts_with("/assets/") { + "index.html" + } else { + // Strip the leading slash for assets + &path[1..] + }; + + // Try to find the file in the embedded directory + if let Some(file) = DEBUGGER_UI.get_file(file_path) { + let content_type = match file_path.split('.').last() { + Some("html") => "text/html", + Some("js") => "application/javascript", + Some("css") => "text/css", + Some("png") => "image/png", + Some("jpg") | Some("jpeg") => "image/jpeg", + Some("svg") => "image/svg+xml", + Some("wasm") => "application/wasm", + _ => "application/octet-stream", + }; + + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, content_type) + .header(header::CACHE_CONTROL, "no-cache") + .body(axum::body::Body::from(file.contents())) + .unwrap() + } else { + (StatusCode::NOT_FOUND, "404 Not Found").into_response() + } +} + +/// No-op for WASM builds (debugger uses BroadcastChannel instead) +#[cfg(target_arch = "wasm32")] +pub fn start_debugger_server() { + // No HTTP server needed for WASM - uses BroadcastChannel +} From 597222907d3939ecad755e40a4856b3ce090df80 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Tue, 14 Oct 2025 15:54:40 +0300 Subject: [PATCH 04/15] emu: remove egui --- Cargo.lock | 731 ++----------------------------------------- Cargo.toml | 10 - src/debugger_core.rs | 258 ++------------- src/lib.rs | 513 +++++++++++++----------------- src/shader.wgsl | 69 +++- 5 files changed, 343 insertions(+), 1238 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 351e3f7..435ee75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,26 +141,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "arboard" -version = "3.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" -dependencies = [ - "clipboard-win", - "image", - "log", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.1", - "parking_lot", - "percent-encoding", - "windows-sys 0.60.2", - "x11rb", -] - [[package]] name = "arrayref" version = "0.3.9" @@ -333,7 +313,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2 0.5.2", + "objc2", ] [[package]] @@ -362,12 +342,6 @@ dependencies = [ "syn", ] -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - [[package]] name = "bytes" version = "1.10.1" @@ -430,15 +404,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "clipboard-win" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" -dependencies = [ - "error-code", -] - [[package]] name = "codespan-reporting" version = "0.12.0" @@ -555,15 +520,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -614,27 +570,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" -[[package]] -name = "dispatch2" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "dlib" version = "0.5.2" @@ -677,74 +612,6 @@ dependencies = [ "sh4-core", ] -[[package]] -name = "ecolor" -version = "0.32.3" -source = "git+https://github.com/emilk/egui?rev=18ea9ff#18ea9ff0bd3e6e1e6bea4ec1cbcf2756707cc5f4" -dependencies = [ - "bytemuck", - "emath", -] - -[[package]] -name = "egui" -version = "0.32.3" -source = "git+https://github.com/emilk/egui?rev=18ea9ff#18ea9ff0bd3e6e1e6bea4ec1cbcf2756707cc5f4" -dependencies = [ - "ahash", - "bitflags 2.9.4", - "emath", - "epaint", - "log", - "nohash-hasher", - "profiling", - "smallvec", - "unicode-segmentation", -] - -[[package]] -name = "egui-wgpu" -version = "0.32.3" -source = "git+https://github.com/emilk/egui?rev=18ea9ff#18ea9ff0bd3e6e1e6bea4ec1cbcf2756707cc5f4" -dependencies = [ - "ahash", - "bytemuck", - "document-features", - "egui", - "epaint", - "log", - "profiling", - "thiserror 1.0.69", - "type-map", - "web-time", - "wgpu", -] - -[[package]] -name = "egui-winit" -version = "0.32.3" -source = "git+https://github.com/emilk/egui?rev=18ea9ff#18ea9ff0bd3e6e1e6bea4ec1cbcf2756707cc5f4" -dependencies = [ - "arboard", - "bytemuck", - "egui", - "log", - "profiling", - "raw-window-handle", - "smithay-clipboard", - "web-time", - "webbrowser", - "winit", -] - -[[package]] -name = "emath" -version = "0.32.3" -source = "git+https://github.com/emilk/egui?rev=18ea9ff#18ea9ff0bd3e6e1e6bea4ec1cbcf2756707cc5f4" -dependencies = [ - "bytemuck", -] - [[package]] name = "env_filter" version = "0.1.3" @@ -768,28 +635,6 @@ dependencies = [ "log", ] -[[package]] -name = "epaint" -version = "0.32.3" -source = "git+https://github.com/emilk/egui?rev=18ea9ff#18ea9ff0bd3e6e1e6bea4ec1cbcf2756707cc5f4" -dependencies = [ - "ab_glyph", - "ahash", - "bytemuck", - "ecolor", - "emath", - "epaint_default_fonts", - "log", - "nohash-hasher", - "parking_lot", - "profiling", -] - -[[package]] -name = "epaint_default_fonts" -version = "0.32.3" -source = "git+https://github.com/emilk/egui?rev=18ea9ff#18ea9ff0bd3e6e1e6bea4ec1cbcf2756707cc5f4" - [[package]] name = "equivalent" version = "1.0.2" @@ -806,63 +651,18 @@ dependencies = [ "windows-sys 0.61.1", ] -[[package]] -name = "error-code" -version = "3.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" - [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - [[package]] name = "find-msvc-tools" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" -[[package]] -name = "flate2" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1268,127 +1068,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "image" -version = "0.25.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" -dependencies = [ - "bytemuck", - "byteorder-lite", - "moxcms", - "num-traits", - "png", - "tiff", -] - [[package]] name = "include_dir" version = "0.7.4" @@ -1587,12 +1266,6 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - [[package]] name = "litrs" version = "0.4.2" @@ -1718,7 +1391,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", - "simd-adler32", ] [[package]] @@ -1732,16 +1404,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "moxcms" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08" -dependencies = [ - "num-traits", - "pxfm", -] - [[package]] name = "naga" version = "26.0.0" @@ -1762,7 +1424,7 @@ dependencies = [ "log", "num-traits", "once_cell", - "rustc-hash 1.1.0", + "rustc-hash", "spirv", "thiserror 2.0.17", "unicode-ident", @@ -1810,12 +1472,6 @@ dependencies = [ "memoffset", ] -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - [[package]] name = "nullDC" version = "2.0.0-pre" @@ -1824,9 +1480,6 @@ dependencies = [ "bytemuck", "console_error_panic_hook", "dreamcast", - "egui", - "egui-wgpu", - "egui-winit", "env_logger", "futures", "git-version", @@ -1914,15 +1567,6 @@ dependencies = [ "objc2-encode", ] -[[package]] -name = "objc2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" -dependencies = [ - "objc2-encode", -] - [[package]] name = "objc2-app-kit" version = "0.2.2" @@ -1932,25 +1576,13 @@ dependencies = [ "bitflags 2.9.4", "block2", "libc", - "objc2 0.5.2", + "objc2", "objc2-core-data", "objc2-core-image", - "objc2-foundation 0.2.2", + "objc2-foundation", "objc2-quartz-core", ] -[[package]] -name = "objc2-app-kit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-core-graphics", - "objc2-foundation 0.3.1", -] - [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -1959,9 +1591,9 @@ checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.9.4", "block2", - "objc2 0.5.2", + "objc2", "objc2-core-location", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -1971,8 +1603,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2", + "objc2-foundation", ] [[package]] @@ -1983,32 +1615,8 @@ checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.9.4", "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" -dependencies = [ - "bitflags 2.9.4", - "dispatch2", - "objc2 0.6.2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" -dependencies = [ - "bitflags 2.9.4", - "dispatch2", - "objc2 0.6.2", - "objc2-core-foundation", - "objc2-io-surface", + "objc2", + "objc2-foundation", ] [[package]] @@ -2018,8 +1626,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2", + "objc2-foundation", "objc2-metal", ] @@ -2030,9 +1638,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ "block2", - "objc2 0.5.2", + "objc2", "objc2-contacts", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -2051,29 +1659,7 @@ dependencies = [ "block2", "dispatch", "libc", - "objc2 0.5.2", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-core-foundation", + "objc2", ] [[package]] @@ -2083,9 +1669,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ "block2", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2", + "objc2-app-kit", + "objc2-foundation", ] [[package]] @@ -2096,8 +1682,8 @@ checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.9.4", "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2", + "objc2-foundation", ] [[package]] @@ -2108,8 +1694,8 @@ checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.9.4", "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2", + "objc2-foundation", "objc2-metal", ] @@ -2119,8 +1705,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2", + "objc2-foundation", ] [[package]] @@ -2131,12 +1717,12 @@ checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.9.4", "block2", - "objc2 0.5.2", + "objc2", "objc2-cloud-kit", "objc2-core-data", "objc2-core-image", "objc2-core-location", - "objc2-foundation 0.2.2", + "objc2-foundation", "objc2-link-presentation", "objc2-quartz-core", "objc2-symbols", @@ -2151,8 +1737,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2", + "objc2-foundation", ] [[package]] @@ -2163,9 +1749,9 @@ checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.9.4", "block2", - "objc2 0.5.2", + "objc2", "objc2-core-location", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -2292,19 +1878,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "png" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" -dependencies = [ - "bitflags 2.9.4", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - [[package]] name = "polling" version = "3.11.0" @@ -2340,15 +1913,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "potential_utf" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" -dependencies = [ - "zerovec", -] - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2388,21 +1952,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" -[[package]] -name = "pxfm" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde" -dependencies = [ - "num-traits", -] - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quick-xml" version = "0.37.5" @@ -2547,12 +2096,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - [[package]] name = "rustix" version = "0.38.44" @@ -2768,12 +2311,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - [[package]] name = "slab" version = "0.4.11" @@ -2820,17 +2357,6 @@ dependencies = [ "xkeysym", ] -[[package]] -name = "smithay-clipboard" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" -dependencies = [ - "libc", - "smithay-client-toolkit", - "wayland-backend", -] - [[package]] name = "smol_str" version = "0.2.2" @@ -2859,12 +2385,6 @@ dependencies = [ "bitflags 2.9.4", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "static_assertions" version = "1.1.0" @@ -2894,17 +2414,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tempfile" version = "3.23.0" @@ -2967,20 +2476,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tiff" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg", -] - [[package]] name = "tiny-skia" version = "0.11.4" @@ -3006,16 +2501,6 @@ dependencies = [ "strict-num", ] -[[package]] -name = "tinystr" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tokio" version = "1.47.1" @@ -3209,15 +2694,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "type-map" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" -dependencies = [ - "rustc-hash 2.1.1", -] - [[package]] name = "typenum" version = "1.19.0" @@ -3248,30 +2724,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -3620,28 +3078,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webbrowser" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf4f3c0ba838e82b4e5ccc4157003fb8c324ee24c058470ffb82820becbde98" -dependencies = [ - "core-foundation 0.10.1", - "jni", - "log", - "ndk-context", - "objc2 0.6.2", - "objc2-foundation 0.3.1", - "url", - "web-sys", -] - -[[package]] -name = "weezl" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" - [[package]] name = "wgpu" version = "26.0.1" @@ -3692,7 +3128,7 @@ dependencies = [ "portable-atomic", "profiling", "raw-window-handle", - "rustc-hash 1.1.0", + "rustc-hash", "smallvec", "thiserror 2.0.17", "wgpu-core-deps-apple", @@ -4146,9 +3582,9 @@ dependencies = [ "libc", "memmap2", "ndk", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2", + "objc2-app-kit", + "objc2-foundation", "objc2-ui-kit", "orbclient", "percent-encoding", @@ -4190,12 +3626,6 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" -[[package]] -name = "writeable" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" - [[package]] name = "x11-dl" version = "2.21.0" @@ -4259,30 +3689,6 @@ version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" -[[package]] -name = "yoke" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.8.27" @@ -4302,72 +3708,3 @@ dependencies = [ "quote", "syn", ] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - -[[package]] -name = "zune-jpeg" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" -dependencies = [ - "zune-core", -] diff --git a/Cargo.toml b/Cargo.toml index 962ba5e..117915a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,11 +50,6 @@ git-version = "0.3" wgpu = "26.0.1" winit = "0.30.12" -# Disable default features globally -egui = { git = "https://github.com/emilk/egui", rev = "18ea9ff", default-features = false } -egui-winit = { git = "https://github.com/emilk/egui", rev = "18ea9ff", default-features = false } -egui-wgpu = { git = "https://github.com/emilk/egui", rev = "18ea9ff", default-features = false } - # Utilities bytemuck = "1.16" pollster = "0.4" @@ -63,11 +58,6 @@ log = "0.4" sha2 = "0.10" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -# Re-enable native-only features (clipboard, etc.) -egui = { git = "https://github.com/emilk/egui", rev = "18ea9ff", features = ["default_fonts" ] } -egui-winit = { git = "https://github.com/emilk/egui", rev = "18ea9ff", features = ["clipboard"] } -egui-wgpu = { git = "https://github.com/emilk/egui", rev = "18ea9ff" } - # Debugger UI server include_dir = "0.7" diff --git a/src/debugger_core.rs b/src/debugger_core.rs index a8b8a12..2c4ee58 100644 --- a/src/debugger_core.rs +++ b/src/debugger_core.rs @@ -316,14 +316,12 @@ struct ServerWatch { expression: String, } -// Server state +// Server state - no more mock register values, uses real emulator pub struct ServerState { breakpoints: Arc>>, watches: Arc>>, - register_values: Arc>>, event_log: Arc>>, category_states: Arc>>, - is_running: Arc>, tick_id: Arc>, next_event_id: Arc>, next_watch_id: Arc>, @@ -332,57 +330,6 @@ pub struct ServerState { impl ServerState { pub fn new() -> Self { - let mut register_values = HashMap::new(); - - // Initialize register values - register_values.insert("dc.sh4.cpu.pc".to_string(), "0x8C0000A0".to_string()); - register_values.insert("dc.sh4.cpu.pr".to_string(), "0x8C0000A2".to_string()); - register_values.insert("dc.sh4.vbr".to_string(), "0x8C000000".to_string()); - register_values.insert("dc.sh4.sr".to_string(), "0x40000000".to_string()); - register_values.insert("dc.sh4.fpscr".to_string(), "0x00040001".to_string()); - register_values.insert("dc.sh4.cpu.gbr".to_string(), "0x8C000100".to_string()); - register_values.insert("dc.sh4.cpu.mach".to_string(), "0x00000000".to_string()); - register_values.insert("dc.sh4.cpu.macl".to_string(), "0x00000000".to_string()); - register_values.insert("dc.sh4.cpu.fpul".to_string(), "0x00000000".to_string()); - register_values.insert( - "dc.sh4.icache.icache_ctrl".to_string(), - "0x00000003".to_string(), - ); - register_values.insert( - "dc.sh4.dcache.dcache_ctrl".to_string(), - "0x00000003".to_string(), - ); - register_values.insert("dc.sh4.dmac.dmaor".to_string(), "0x8201".to_string()); - register_values.insert("dc.holly.holly_id".to_string(), "0x00050000".to_string()); - register_values.insert("dc.holly.dmac_ctrl".to_string(), "0x00000001".to_string()); - register_values.insert("dc.holly.dmac.dmaor".to_string(), "0x8201".to_string()); - register_values.insert("dc.holly.dmac.chcr0".to_string(), "0x00000001".to_string()); - register_values.insert( - "dc.holly.ta.ta_list_base".to_string(), - "0x0C000000".to_string(), - ); - register_values.insert( - "dc.holly.ta.ta_status".to_string(), - "0x00000000".to_string(), - ); - register_values.insert( - "dc.holly.core.pvr_ctrl".to_string(), - "0x00000001".to_string(), - ); - register_values.insert( - "dc.holly.core.pvr_status".to_string(), - "0x00010000".to_string(), - ); - register_values.insert("dc.aica.aica_ctrl".to_string(), "0x00000002".to_string()); - register_values.insert("dc.aica.aica_status".to_string(), "0x00000001".to_string()); - register_values.insert("dc.aica.arm7.pc".to_string(), "0x00200010".to_string()); - register_values.insert("dc.aica.channels.ch0_vol".to_string(), "0x7F".to_string()); - register_values.insert("dc.aica.channels.ch1_vol".to_string(), "0x6A".to_string()); - register_values.insert("dc.aica.dsp.step".to_string(), "0x000".to_string()); - register_values.insert("dc.aica.dsp.dsp_acc".to_string(), "0x1F".to_string()); - register_values.insert("dc.sysclk".to_string(), "200MHz".to_string()); - register_values.insert("dc.asic_rev".to_string(), "0x0001".to_string()); - // Initialize default watches let mut watches = HashMap::new(); let mut next_watch_id = 1; @@ -415,10 +362,8 @@ impl ServerState { Self { breakpoints: Arc::new(Mutex::new(HashMap::new())), watches: Arc::new(Mutex::new(watches)), - register_values: Arc::new(Mutex::new(register_values)), event_log: Arc::new(Mutex::new(event_log)), category_states: Arc::new(Mutex::new(category_states)), - is_running: Arc::new(Mutex::new(true)), tick_id: Arc::new(Mutex::new(0)), next_event_id: Arc::new(Mutex::new(next_event_id)), next_watch_id: Arc::new(Mutex::new(next_watch_id)), @@ -426,19 +371,24 @@ impl ServerState { } } - fn get_register_value(&self, path: &str, name: &str) -> String { - let key = format!("{}.{}", path, name.to_lowercase()); - self.register_values - .lock() - .unwrap() - .get(&key) - .cloned() - .unwrap_or_else(|| "0x00000000".to_string()) - } + // Get register value from the actual emulator + fn get_register_value(&self, dreamcast_ptr: usize, path: &str, name: &str) -> String { + #[cfg(not(target_arch = "wasm32"))] + if dreamcast_ptr != 0 { + let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; + if path == "dc.sh4.cpu" || path == "dc.sh4" { + if let Some(value) = crate::dreamcast::get_sh4_register(dreamcast, name) { + return format!("0x{:08X}", value); + } + } else if path == "dc.aica.arm7" { + if let Some(value) = crate::dreamcast::get_arm_register(dreamcast, name) { + return format!("0x{:08X}", value); + } + } + } - fn set_register_value(&self, path: &str, name: &str, value: String) { - let key = format!("{}.{}", path, name.to_lowercase()); - self.register_values.lock().unwrap().insert(key, value); + // Return default value if emulator not available or register not found + "0x00000000".to_string() } fn evaluate_watch_expression(&self, dreamcast_ptr: usize, expression: &str) -> String { @@ -460,29 +410,15 @@ impl ServerState { ("dc.sh4.cpu".to_string(), parts[0].to_string()) }; - // Try to get value from actual emulator if available - #[cfg(not(target_arch = "wasm32"))] - if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; - if path == "dc.sh4.cpu" { - if let Some(value) = crate::dreamcast::get_sh4_register(dreamcast, &name) { - return format!("0x{:08X}", value); - } - } else if path == "dc.aica.arm7" { - if let Some(value) = crate::dreamcast::get_arm_register(dreamcast, &name) { - return format!("0x{:08X}", value); - } - } - } - - // Fall back to mock values - self.get_register_value(&path, &name) + self.get_register_value(dreamcast_ptr, &path, &name) } - fn build_device_tree(&self) -> Vec { + fn build_device_tree(&self, dreamcast_ptr: usize) -> Vec { + let get_reg = |path: &str, name: &str| self.get_register_value(dreamcast_ptr, path, name); + let register = |path: &str, name: &str, width: u32| RegisterValue { name: name.to_string(), - value: self.get_register_value(path, name), + value: get_reg(path, name), width, flags: None, metadata: None, @@ -847,49 +783,6 @@ impl ServerState { }] } - fn set_running(&self, running: bool) { - let mut guard = self.is_running.lock().unwrap(); - *guard = running; - } - - fn increment_program_counter(&self, target: &str) { - let target_lower = target.to_ascii_lowercase(); - if target_lower.contains("sh4") { - if let Some(stripped) = self - .get_register_value("dc.sh4.cpu", "PC") - .strip_prefix("0x") - { - if let Ok(pc) = u32::from_str_radix(stripped, 16) { - let base = 0x8C0000A0; - let offset = pc.wrapping_sub(base); - let new_pc = base + ((offset + 2) % (8 * 2)); - self.set_register_value("dc.sh4.cpu", "PC", format!("0x{:08X}", new_pc)); - } - } - } else if target_lower.contains("arm7") { - if let Some(stripped) = self - .get_register_value("dc.aica.arm7", "PC") - .strip_prefix("0x") - { - if let Ok(pc) = u32::from_str_radix(stripped, 16) { - let base = 0x0020_0010; - let offset = pc.wrapping_sub(base); - let new_pc = base + ((offset + 4) % (8 * 4)); - self.set_register_value("dc.aica.arm7", "PC", format!("0x{:08X}", new_pc)); - } - } - } else if target_lower.contains("dsp") { - if let Some(stripped) = self - .get_register_value("dc.aica.dsp", "STEP") - .strip_prefix("0x") - { - if let Ok(step) = u32::from_str_radix(stripped, 16) { - let new_step = (step + 1) % 8; - self.set_register_value("dc.aica.dsp", "STEP", format!("0x{:03X}", new_step)); - } - } - } - } #[allow(dead_code)] fn next_event_id(&self) -> u64 { @@ -914,7 +807,7 @@ impl ServerState { } pub fn build_tick(&self, dreamcast_ptr: usize, hit_breakpoint_id: Option) -> DebuggerTick { - let device_tree = self.build_device_tree(); + let device_tree = self.build_device_tree(dreamcast_ptr); let all_registers = collect_registers_from_tree(&device_tree); let mut registers_by_id: HashMap> = HashMap::new(); @@ -1027,17 +920,11 @@ impl ServerState { .map(|value| value as u64) .unwrap_or(0x8C00_00A0) } else { - self.get_register_value("dc.sh4.cpu", "PC") - .strip_prefix("0x") - .and_then(|value| u64::from_str_radix(value, 16).ok()) - .unwrap_or(0x8C00_00A0) + 0x8C00_00A0 }; #[cfg(target_arch = "wasm32")] - let sh4_pc = self.get_register_value("dc.sh4.cpu", "PC") - .strip_prefix("0x") - .and_then(|value| u64::from_str_radix(value, 16).ok()) - .unwrap_or(0x8C00_00A0); + let sh4_pc = 0x8C00_00A0; let sh4_frames = (0..16) .map(|index| CallstackFrame { @@ -1061,17 +948,11 @@ impl ServerState { .map(|value| value as u64) .unwrap_or(0x0020_0010) } else { - self.get_register_value("dc.aica.arm7", "PC") - .strip_prefix("0x") - .and_then(|value| u64::from_str_radix(value, 16).ok()) - .unwrap_or(0x0020_0010) + 0x0020_0010 }; #[cfg(target_arch = "wasm32")] - let arm7_pc = self.get_register_value("dc.aica.arm7", "PC") - .strip_prefix("0x") - .and_then(|value| u64::from_str_radix(value, 16).ok()) - .unwrap_or(0x0020_0010); + let arm7_pc = 0x0020_0010; let arm7_frames = (0..16) .map(|index| CallstackFrame { @@ -1102,11 +983,11 @@ impl ServerState { let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; crate::dreamcast::is_dreamcast_running(dreamcast) } else { - *self.is_running.lock().unwrap() + false // Default to paused when no emulator context }; #[cfg(target_arch = "wasm32")] - let is_running = *self.is_running.lock().unwrap(); + let is_running = false; // Default to paused for WASM DebuggerTick { tick_id, @@ -1289,7 +1170,7 @@ pub fn handle_request( match request.method.as_str() { "debugger.describe" => { - let device_tree = state.build_device_tree(); + let device_tree = state.build_device_tree(dreamcast_ptr); Ok(( json!({ "emulator": { @@ -1436,70 +1317,13 @@ pub fn handle_request( } "state.editWatch" => { - let watch_id = params - .get("watchId") - .and_then(|value| value.as_u64()) - .map(|value| value as u32); - let value = params - .get("value") - .and_then(|value| value.as_str()) - .unwrap_or(""); - - if let Some(id) = watch_id { - let expression = { - let watches = state.watches.lock().unwrap(); - watches.get(&id).map(|watch| watch.expression.clone()) - }; - - if let Some(expr) = expression { - let parts: Vec<&str> = expr.split('.').collect(); - let (path, name) = if parts.len() > 1 { - let name = parts.last().unwrap(); - let path = parts[..parts.len() - 1].join("."); - (path, name.to_string()) - } else { - ("dc.sh4.cpu".to_string(), parts[0].to_string()) - }; - let key = format!("{}.{}", path, name.to_lowercase()); - - { - let registers = state.register_values.lock().unwrap(); - if !registers.contains_key(&key) { - return Ok(( - json!({ - "error": { - "code": -32602, - "message": format!( - "Cannot edit non-register expression \"{}\"", - expr - ), - } - }), - false, - )); - } - } - - state.set_register_value(&path, &name, value.to_string()); - return Ok((json!({}), true)); - } - - return Ok(( - json!({ - "error": { - "code": -32602, - "message": format!("Watch \"{}\" not found", id), - } - }), - false, - )); - } - + // Editing watch values not supported when using real emulator + // This would require writing back to emulator memory/registers Ok(( json!({ "error": { "code": -32602, - "message": "Watch not found or cannot edit", + "message": "Watch editing not supported", } }), false, @@ -1551,29 +1375,16 @@ pub fn handle_request( let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; crate::dreamcast::set_dreamcast_running(dreamcast, false); } - state.set_running(false); Ok((json!({}), true)) } "control.step" | "control.stepOver" | "control.stepOut" => { - let target = params - .get("target") - .and_then(|value| value.as_str()) - .unwrap_or("sh4"); - #[cfg(not(target_arch = "wasm32"))] if dreamcast_ptr != 0 { let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; crate::dreamcast::step_dreamcast(dreamcast); crate::dreamcast::set_dreamcast_running(dreamcast, false); - } else { - state.increment_program_counter(target); } - - #[cfg(target_arch = "wasm32")] - state.increment_program_counter(target); - - state.set_running(false); Ok((json!({}), true)) } @@ -1583,7 +1394,6 @@ pub fn handle_request( let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; crate::dreamcast::set_dreamcast_running(dreamcast, true); } - state.set_running(true); Ok((json!({}), true)) } diff --git a/src/lib.rs b/src/lib.rs index d19c0b7..b8be4dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,6 @@ use winit::{ }; use git_version::git_version; -use wgpu::util::DeviceExt; pub use dreamcast; use dreamcast::{Dreamcast, present_for_texture, run_slice_dreamcast}; @@ -47,59 +46,28 @@ struct State { config: wgpu::SurfaceConfiguration, size: winit::dpi::PhysicalSize, render_pipeline: wgpu::RenderPipeline, - vertex_buffer: wgpu::Buffer, - bind_group: wgpu::BindGroup, - egui_renderer: egui_wgpu::Renderer, - egui_state: egui_winit::State, - egui_ctx: egui::Context, - // UI state - clear_color: [f32; 3], - show_triangle: bool, - framebuffer: egui::TextureHandle, + bind_group_layout: wgpu::BindGroupLayout, + uniform_bind_group_layout: wgpu::BindGroupLayout, + uniform_buffer: wgpu::Buffer, + uniform_bind_group: wgpu::BindGroup, + framebuffer_texture: wgpu::Texture, + framebuffer_bind_group: wgpu::BindGroup, + framebuffer_width: u32, + framebuffer_height: u32, } #[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] -struct Vertex { - position: [f32; 3], - tex_coords: [f32; 2], +struct Uniforms { + window_width: f32, + window_height: f32, + fb_width: f32, + fb_height: f32, } -impl Vertex { - fn desc() -> wgpu::VertexBufferLayout<'static> { - wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[ - wgpu::VertexAttribute { - offset: 0, - shader_location: 0, - format: wgpu::VertexFormat::Float32x3, - }, - wgpu::VertexAttribute { - offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, - shader_location: 1, - format: wgpu::VertexFormat::Float32x2, - }, - ], - } - } -} - -const VERTICES: &[Vertex] = &[ - Vertex { - position: [0.0, 0.5, 0.0], - tex_coords: [0.5, 0.0], - }, - Vertex { - position: [-0.5, -0.5, 0.0], - tex_coords: [0.0, 1.0], - }, - Vertex { - position: [0.5, -0.5, 0.0], - tex_coords: [1.0, 1.0], - }, -]; +// Framebuffer dimensions - default Dreamcast resolution +const DEFAULT_FB_WIDTH: u32 = 640; +const DEFAULT_FB_HEIGHT: u32 = 480; // In your initialization code (probably in main or new) #[cfg(target_arch = "wasm32")] @@ -180,25 +148,11 @@ impl State { log::info!("Surface size: {}x{}", config.width, config.height); surface.configure(&device, &config); - // Checker texture - let texture_size = 256u32; - let texture_data: Vec = (0..texture_size * texture_size) - .flat_map(|i| { - let x = i % texture_size; - let y = i / texture_size; - let checker = ((x / 32) + (y / 32)) % 2 == 0; - if checker { - [255, 100, 100, 255] - } else { - [100, 100, 255, 255] - } - }) - .collect(); - - let texture = device.create_texture(&wgpu::TextureDescriptor { + // Create framebuffer texture + let framebuffer_texture = device.create_texture(&wgpu::TextureDescriptor { size: wgpu::Extent3d { - width: texture_size, - height: texture_size, + width: DEFAULT_FB_WIDTH, + height: DEFAULT_FB_HEIGHT, depth_or_array_layers: 1, }, mip_level_count: 1, @@ -206,34 +160,16 @@ impl State { dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8UnormSrgb, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, - label: Some("texture"), + label: Some("framebuffer_texture"), view_formats: &[], }); - queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: &texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - &texture_data, - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(4 * texture_size), - rows_per_image: Some(texture_size), - }, - wgpu::Extent3d { - width: texture_size, - height: texture_size, - depth_or_array_layers: 1, - }, - ); - - let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let framebuffer_view = framebuffer_texture.create_view(&wgpu::TextureViewDescriptor::default()); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Linear, + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, ..Default::default() }); @@ -256,22 +192,62 @@ impl State { count: None, }, ], - label: Some("texture_bind_group_layout"), + label: Some("framebuffer_bind_group_layout"), }); - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + let framebuffer_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::TextureView(&texture_view), + resource: wgpu::BindingResource::TextureView(&framebuffer_view), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, ], - label: Some("texture_bind_group"), + label: Some("framebuffer_bind_group"), + }); + + // Create uniform buffer for window/framebuffer dimensions + let uniforms = Uniforms { + window_width: size.width as f32, + window_height: size.height as f32, + fb_width: DEFAULT_FB_WIDTH as f32, + fb_height: DEFAULT_FB_HEIGHT as f32, + }; + + let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Uniform Buffer"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + queue.write_buffer(&uniform_buffer, 0, bytemuck::cast_slice(&[uniforms])); + + let uniform_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + label: Some("uniform_bind_group_layout"), + }); + + let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &uniform_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }], + label: Some("uniform_bind_group"), }); let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { @@ -282,7 +258,7 @@ impl State { let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Render Pipeline Layout"), - bind_group_layouts: &[&bind_group_layout], + bind_group_layouts: &[&bind_group_layout, &uniform_bind_group_layout], push_constant_ranges: &[], }); @@ -292,7 +268,7 @@ impl State { vertex: wgpu::VertexState { module: &shader, entry_point: Some("vs_main"), - buffers: &[Vertex::desc()], + buffers: &[], compilation_options: Default::default(), }, fragment: Some(wgpu::FragmentState { @@ -312,30 +288,6 @@ impl State { cache: None, }); - let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Vertex Buffer"), - contents: bytemuck::cast_slice(VERTICES), - usage: wgpu::BufferUsages::VERTEX, - }); - - let egui_ctx = egui::Context::default(); - let egui_state = egui_winit::State::new( - egui_ctx.clone(), - egui::ViewportId::ROOT, - &*window, - Some(window.scale_factor() as f32), - None, - None, - ); - - let egui_renderer = egui_wgpu::Renderer::new(&device, config.format, None, 1, false); - - let framebuffer: egui::TextureHandle = egui_ctx.load_texture( - "framebuffer", - egui::ColorImage::new([640, 480], vec![egui::Color32::BLACK; 640 * 480]), - egui::TextureOptions::NEAREST, - ); - Self { window, surface, @@ -344,14 +296,14 @@ impl State { config, size, render_pipeline, - vertex_buffer, - bind_group, - egui_renderer, - egui_state, - egui_ctx, - clear_color: [0.1, 0.2, 0.3], - show_triangle: true, - framebuffer, + bind_group_layout, + uniform_bind_group_layout, + uniform_buffer, + uniform_bind_group, + framebuffer_texture, + framebuffer_bind_group, + framebuffer_width: DEFAULT_FB_WIDTH, + framebuffer_height: DEFAULT_FB_HEIGHT, } } @@ -361,6 +313,15 @@ impl State { self.config.width = new_size.width; self.config.height = new_size.height; self.surface.configure(&self.device, &self.config); + + // Update uniforms with new window size + let uniforms = Uniforms { + window_width: new_size.width as f32, + window_height: new_size.height as f32, + fb_width: self.framebuffer_width as f32, + fb_height: self.framebuffer_height as f32, + }; + self.queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms])); } } @@ -370,60 +331,22 @@ impl State { .texture .create_view(&wgpu::TextureViewDescriptor::default()); - // Begin egui frame - let raw_input = self.egui_state.take_egui_input(&*self.window); - let egui_output = self.egui_ctx.run(raw_input, |ctx| { - egui::Window::new("Framebuffer").show(ctx, |ui| { - ui.image((self.framebuffer.id(), egui::vec2(640.0, 480.0))); - }); - }); - - // Upload egui textures and meshes let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("main encoder"), + label: Some("Render Encoder"), }); - let screen_desc = egui_wgpu::ScreenDescriptor { - size_in_pixels: [self.config.width, self.config.height], - // FIX 1: Use egui Context for ppp - pixels_per_point: self.egui_ctx.pixels_per_point(), - }; - - for (id, image_delta) in &egui_output.textures_delta.set { - self.egui_renderer - .update_texture(&self.device, &self.queue, *id, image_delta); - } - - let paint_jobs = self - .egui_ctx - .tessellate(egui_output.shapes, self.egui_ctx.pixels_per_point()); - self.egui_renderer.update_buffers( - &self.device, - &self.queue, - &mut encoder, - &paint_jobs, - &screen_desc, - ); - - // 1) Clear + draw triangle (if enabled) + // Single render pass - draw fullscreen quad with framebuffer texture { let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("triangle pass"), + label: Some("Framebuffer Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &view, resolve_target: None, - // FIX 2: New field in wgpu depth_slice: None, ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: self.clear_color[0] as f64, - g: self.clear_color[1] as f64, - b: self.clear_color[2] as f64, - a: 1.0, - }), - // FIX 3: StoreOp, not bool + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, })], @@ -432,78 +355,93 @@ impl State { occlusion_query_set: None, }); - if self.show_triangle { - rpass.set_pipeline(&self.render_pipeline); - rpass.set_bind_group(0, &self.bind_group, &[]); - rpass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); - rpass.draw(0..3, 0..1); - } + rpass.set_pipeline(&self.render_pipeline); + rpass.set_bind_group(0, &self.framebuffer_bind_group, &[]); + rpass.set_bind_group(1, &self.uniform_bind_group, &[]); + rpass.draw(0..6, 0..1); // Draw 6 vertices for fullscreen quad (2 triangles) } - // 2) Draw egui on top (separate pass, load existing color) - { - let rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("egui pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - depth_slice: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); + self.queue.submit(Some(encoder.finish())); + output.present(); - let mut rpass = rpass.forget_lifetime(); + Ok(()) + } - // FIX 4: render into a RenderPass, not encoder+view - self.egui_renderer - .render(&mut rpass, &paint_jobs, &screen_desc); - } + fn update_framebuffer(&mut self, rgba_data: &[u8], width: usize, height: usize) { + // If framebuffer size changed, recreate texture and bind group + if width as u32 != self.framebuffer_width || height as u32 != self.framebuffer_height { + self.framebuffer_width = width as u32; + self.framebuffer_height = height as u32; + + self.framebuffer_texture = self.device.create_texture(&wgpu::TextureDescriptor { + size: wgpu::Extent3d { + width: self.framebuffer_width, + height: self.framebuffer_height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + label: Some("framebuffer_texture"), + view_formats: &[], + }); - // Submit - self.queue.submit(Some(encoder.finish())); - output.present(); + let framebuffer_view = self.framebuffer_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor { + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + ..Default::default() + }); - // Cleanup egui textures - for id in &egui_output.textures_delta.free { - self.egui_renderer.free_texture(id); - } + self.framebuffer_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &self.bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&framebuffer_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + label: Some("framebuffer_bind_group"), + }); - // log::info!("Render"); - // let frame = self.surface.get_current_texture().unwrap(); - // let view = frame.texture.create_view(&Default::default()); - - // let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - // label: Some("Render Encoder"), - // }); - - // { - // let _rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - // label: Some("Clear Pass"), - // color_attachments: &[Some(wgpu::RenderPassColorAttachment { - // view: &view, - // resolve_target: None, - // depth_slice: None, - // ops: wgpu::Operations { - // load: wgpu::LoadOp::Clear(wgpu::Color::RED), // force bright red - // store: wgpu::StoreOp::Store, - // }, - // })], - // depth_stencil_attachment: None, - // timestamp_writes: None, - // occlusion_query_set: None, - // }); - // } - - // self.queue.submit(Some(encoder.finish())); - // frame.present(); + // Update uniforms with new framebuffer size + let uniforms = Uniforms { + window_width: self.config.width as f32, + window_height: self.config.height as f32, + fb_width: self.framebuffer_width as f32, + fb_height: self.framebuffer_height as f32, + }; + self.queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms])); + } - Ok(()) + // Write framebuffer data to texture + self.queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &self.framebuffer_texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + rgba_data, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(4 * self.framebuffer_width), + rows_per_image: Some(self.framebuffer_height), + }, + wgpu::Extent3d { + width: self.framebuffer_width, + height: self.framebuffer_height, + depth_or_array_layers: 1, + }, + ); } } @@ -606,82 +544,63 @@ impl ApplicationHandler for AppHandle { ) { let mut app = self.0.borrow_mut(); - if let Some(state) = app.state.as_mut() { - if !state - .egui_state - .on_window_event(&state.window, &event) - .consumed - { - match event { - WindowEvent::CloseRequested - | WindowEvent::KeyboardInput { - event: - KeyEvent { - state: ElementState::Pressed, - physical_key: PhysicalKey::Code(KeyCode::Escape), - .. - }, - .. - } => { - event_loop.exit(); - } + if let Some(_state) = app.state.as_mut() { + match event { + WindowEvent::CloseRequested + | WindowEvent::KeyboardInput { + event: + KeyEvent { + state: ElementState::Pressed, + physical_key: PhysicalKey::Code(KeyCode::Escape), + .. + }, + .. + } => { + event_loop.exit(); + } - WindowEvent::Resized(size) => { - if let Some(state) = app.state.as_mut() { - state.resize(size); - } + WindowEvent::Resized(size) => { + if let Some(state) = app.state.as_mut() { + state.resize(size); } + } - WindowEvent::RedrawRequested => { - let mut image: Option = None; - - if !app.dreamcast.is_null() { - let dreamcast = app.dreamcast; - run_slice_dreamcast(dreamcast); - } + WindowEvent::RedrawRequested => { + // Run emulator slice + if !app.dreamcast.is_null() { + let dreamcast = app.dreamcast; + run_slice_dreamcast(dreamcast); + } - if let Some((rgba, width, height)) = present_for_texture() { - let pixel_count = width.saturating_mul(height); - if pixel_count > 0 && rgba.len() >= pixel_count * 4 { - let mut pixels = Vec::with_capacity(pixel_count); - for chunk in rgba.chunks_exact(4) { - pixels.push(egui::Color32::from_rgba_unmultiplied( - chunk[0], chunk[1], chunk[2], chunk[3], - )); - } - image = Some(egui::ColorImage { - size: [width, height], - pixels, - source_size: egui::vec2(width as f32, height as f32), - }); + // Get framebuffer from emulator and update texture + if let Some((rgba, width, height)) = present_for_texture() { + let pixel_count = width.saturating_mul(height); + if pixel_count > 0 && rgba.len() >= pixel_count * 4 { + if let Some(state) = app.state.as_mut() { + state.update_framebuffer(&rgba, width, height); } } + } - if let Some(state) = app.state.as_mut() { - // Update framebuffer texture - if let Some(image) = image { - state.framebuffer.set(image, egui::TextureOptions::NEAREST); + // Render + if let Some(state) = app.state.as_mut() { + match state.render() { + Ok(()) => {} + Err(wgpu::SurfaceError::Lost) | Err(wgpu::SurfaceError::Outdated) => { + state.resize(state.size); } - // Render - match state.render() { - Ok(()) => {} - Err(wgpu::SurfaceError::Lost) - | Err(wgpu::SurfaceError::Outdated) => { - state.resize(state.size); - } - Err(wgpu::SurfaceError::OutOfMemory) => { - // event_loop.exit(); - } - Err(wgpu::SurfaceError::Timeout) => { - // skip frame - } - Err(_) => {} + Err(wgpu::SurfaceError::OutOfMemory) => { + event_loop.exit(); } + Err(wgpu::SurfaceError::Timeout) => { + // skip frame + } + Err(_) => {} } } - - _ => {} } + + _ => {} } } } diff --git a/src/shader.wgsl b/src/shader.wgsl index e033f1a..4e183f6 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -1,6 +1,8 @@ -struct VertexInput { - @location(0) position: vec3, - @location(1) tex_coords: vec2, +struct Uniforms { + window_width: f32, + window_height: f32, + fb_width: f32, + fb_height: f32, } struct VertexOutput { @@ -8,20 +10,67 @@ struct VertexOutput { @location(0) tex_coords: vec2, } +@group(1) @binding(0) +var uniforms: Uniforms; + +// Generate a fullscreen quad (two triangles) +// 6 vertices: 0,1,2 for first triangle, 2,1,3 for second triangle @vertex -fn vs_main(model: VertexInput) -> VertexOutput { +fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { var out: VertexOutput; - out.tex_coords = model.tex_coords; - out.clip_position = vec4(model.position, 1.0); + + // Vertex positions for two triangles forming a quad + // Triangle 1: (0,0), (1,0), (0,1) + // Triangle 2: (0,1), (1,0), (1,1) + var positions = array, 6>( + vec2(0.0, 0.0), // bottom-left + vec2(1.0, 0.0), // bottom-right + vec2(0.0, 1.0), // top-left + vec2(0.0, 1.0), // top-left + vec2(1.0, 0.0), // bottom-right + vec2(1.0, 1.0) // top-right + ); + + let pos = positions[vertex_index]; + + // Convert from 0-1 range to -1 to 1 clip space + let x = pos.x * 2.0 - 1.0; + let y = pos.y * 2.0 - 1.0; + + // Calculate aspect ratios + let window_aspect = uniforms.window_width / uniforms.window_height; + let fb_aspect = uniforms.fb_width / uniforms.fb_height; // 4:3 = 1.333... + + // Calculate scale to maintain 4:3 aspect ratio + var scale_x = 1.0; + var scale_y = 1.0; + + if (window_aspect > fb_aspect) { + // Window is wider than 4:3 - add pillarboxing (black bars on sides) + scale_x = fb_aspect / window_aspect; + } else { + // Window is taller than 4:3 - add letterboxing (black bars top/bottom) + scale_y = window_aspect / fb_aspect; + } + + // Apply scaling to maintain aspect ratio + let scaled_x = x * scale_x; + let scaled_y = y * scale_y; + + out.clip_position = vec4(scaled_x, scaled_y, 0.0, 1.0); + + // Texture coordinates (flip Y for correct orientation) + out.tex_coords = vec2(pos.x, 1.0 - pos.y); + return out; } @group(0) @binding(0) -var t_diffuse: texture_2d; +var t_framebuffer: texture_2d; @group(0) @binding(1) -var s_diffuse: sampler; +var s_framebuffer: sampler; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return textureSample(t_diffuse, s_diffuse, in.tex_coords); -} \ No newline at end of file + return textureSample(t_framebuffer, s_framebuffer, in.tex_coords); +} From 861c30431151e8e4e9812cb2095d7035a8f643e8 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Tue, 14 Oct 2025 17:31:17 +0300 Subject: [PATCH 05/15] emu: don't mock on wasm debugger --- Cargo.toml | 4 - src/debugger_core.rs | 283 ++++++------------------------------------- 2 files changed, 38 insertions(+), 249 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 117915a..d813d97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,10 +32,6 @@ opt-level = 3 strip = false debug = true -[profile.release.package."*"] -debug = false -strip = true - [features] default = ["refsw2-rust"] refsw2-rust = ["dreamcast/refsw2-rust"] diff --git a/src/debugger_core.rs b/src/debugger_core.rs index 2c4ee58..abe7de2 100644 --- a/src/debugger_core.rs +++ b/src/debugger_core.rs @@ -3,7 +3,6 @@ use serde::{Deserialize, Serialize}; use serde_json::json; -use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; @@ -54,94 +53,7 @@ mod panel_ids { pub const ARM7_CALLSTACK: &str = "arm7-callstack"; } -type FrameEventGenerator = fn(u64) -> (&'static str, &'static str, String); - -fn frame_event_ta(counter: u64) -> (&'static str, &'static str, String) { - ( - "ta", - "info", - format!("TA/END_LIST tile {}", (counter % 32) as usize), - ) -} - -fn frame_event_core(counter: u64) -> (&'static str, &'static str, String) { - let phase = match counter % 3 { - 0 => "START_RENDER", - 1 => "QUEUE_SUBMISSION", - _ => "END_RENDER", - }; - ( - "core", - if phase == "QUEUE_SUBMISSION" { - "trace" - } else { - "info" - }, - format!("CORE/{}", phase), - ) -} - -fn frame_event_dsp(counter: u64) -> (&'static str, &'static str, String) { - ( - "dsp", - "trace", - format!("DSP/STEP pipeline advanced ({})", counter % 8), - ) -} - -fn frame_event_aica(counter: u64) -> (&'static str, &'static str, String) { - ( - "aica", - "info", - format!("AICA/SGC/STEP channel {}", (counter % 2) as usize), - ) -} - -fn frame_event_sh4(counter: u64) -> (&'static str, &'static str, String) { - ( - "sh4", - "warn", - format!("SH4/INTERRUPT IRQ{} asserted", (counter % 6) + 1), - ) -} - -fn frame_event_holly(counter: u64) -> (&'static str, &'static str, String) { - ( - "holly", - "info", - format!("HOLLY/START_RENDER pass {}", (counter % 4) + 1), - ) -} - -const FRAME_EVENT_GENERATORS: &[FrameEventGenerator] = &[ - frame_event_ta, - frame_event_core, - frame_event_dsp, - frame_event_aica, - frame_event_sh4, - frame_event_holly, -]; - -fn create_frame_event_with_id(event_id: u64) -> EventLogEntry { - let generator = FRAME_EVENT_GENERATORS[(event_id as usize) % FRAME_EVENT_GENERATORS.len()]; - let (subsystem, severity, message) = generator(event_id); - EventLogEntry { - event_id: event_id.to_string(), - timestamp: current_timestamp_ms(), - subsystem: subsystem.to_string(), - severity: severity.to_string(), - message, - metadata: None, - } -} - -fn initial_event_log() -> (Vec, u64) { - let mut log = Vec::new(); - for id in 1..=6 { - log.push(create_frame_event_with_id(id)); - } - (log, 7) -} +// Mock event generation removed - use real emulator events only // For WASM, we use js_sys::Date::now() instead of SystemTime #[cfg(target_arch = "wasm32")] @@ -356,8 +268,9 @@ impl ServerState { ); } - // Initialize event log with sample entries - let (event_log, next_event_id) = initial_event_log(); + // Initialize with empty event log - use real emulator events only + let event_log = Vec::new(); + let next_event_id = 1; Self { breakpoints: Arc::new(Mutex::new(HashMap::new())), @@ -371,17 +284,16 @@ impl ServerState { } } - // Get register value from the actual emulator + // Get register value from the actual emulator - works for both native and WASM builds fn get_register_value(&self, dreamcast_ptr: usize, path: &str, name: &str) -> String { - #[cfg(not(target_arch = "wasm32"))] if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; + let dreamcast = dreamcast_ptr as *mut dreamcast::Dreamcast; if path == "dc.sh4.cpu" || path == "dc.sh4" { - if let Some(value) = crate::dreamcast::get_sh4_register(dreamcast, name) { + if let Some(value) = dreamcast::get_sh4_register(dreamcast, name) { return format!("0x{:08X}", value); } } else if path == "dc.aica.arm7" { - if let Some(value) = crate::dreamcast::get_arm_register(dreamcast, name) { + if let Some(value) = dreamcast::get_arm_register(dreamcast, name) { return format!("0x{:08X}", value); } } @@ -815,9 +727,8 @@ impl ServerState { registers_by_id.insert(path, registers); } - #[cfg(not(target_arch = "wasm32"))] if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; + let dreamcast = dreamcast_ptr as *mut dreamcast::Dreamcast; let sh4_registers = [ ("PC", 32), @@ -832,7 +743,7 @@ impl ServerState { ]; let mut sh4_cpu_regs = Vec::new(); for (name, width) in sh4_registers { - if let Some(value) = crate::dreamcast::get_sh4_register(dreamcast, name) { + if let Some(value) = dreamcast::get_sh4_register(dreamcast, name) { sh4_cpu_regs.push(RegisterValue { name: name.to_string(), value: format!("0x{:08X}", value), @@ -844,7 +755,7 @@ impl ServerState { } for idx in 0..16 { let reg_name = format!("R{}", idx); - if let Some(value) = crate::dreamcast::get_sh4_register(dreamcast, ®_name) { + if let Some(value) = dreamcast::get_sh4_register(dreamcast, ®_name) { sh4_cpu_regs.push(RegisterValue { name: reg_name, value: format!("0x{:08X}", value), @@ -859,7 +770,7 @@ impl ServerState { } let mut arm_regs = Vec::new(); - if let Some(value) = crate::dreamcast::get_arm_register(dreamcast, "PC") { + if let Some(value) = dreamcast::get_arm_register(dreamcast, "PC") { arm_regs.push(RegisterValue { name: "PC".to_string(), value: format!("0x{:08X}", value), @@ -870,7 +781,7 @@ impl ServerState { } for idx in 0..16 { let reg_name = format!("R{}", idx); - if let Some(value) = crate::dreamcast::get_arm_register(dreamcast, ®_name) { + if let Some(value) = dreamcast::get_arm_register(dreamcast, ®_name) { arm_regs.push(RegisterValue { name: reg_name, value: format!("0x{:08X}", value), @@ -913,19 +824,15 @@ impl ServerState { let mut callstacks = HashMap::new(); - #[cfg(not(target_arch = "wasm32"))] let sh4_pc = if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; - crate::dreamcast::get_sh4_register(dreamcast, "PC") + let dreamcast = dreamcast_ptr as *mut dreamcast::Dreamcast; + dreamcast::get_sh4_register(dreamcast, "PC") .map(|value| value as u64) .unwrap_or(0x8C00_00A0) } else { 0x8C00_00A0 }; - #[cfg(target_arch = "wasm32")] - let sh4_pc = 0x8C00_00A0; - let sh4_frames = (0..16) .map(|index| CallstackFrame { index, @@ -941,19 +848,15 @@ impl ServerState { .collect::>(); callstacks.insert("sh4".to_string(), sh4_frames); - #[cfg(not(target_arch = "wasm32"))] let arm7_pc = if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; - crate::dreamcast::get_arm_register(dreamcast, "PC") + let dreamcast = dreamcast_ptr as *mut dreamcast::Dreamcast; + dreamcast::get_arm_register(dreamcast, "PC") .map(|value| value as u64) .unwrap_or(0x0020_0010) } else { 0x0020_0010 }; - #[cfg(target_arch = "wasm32")] - let arm7_pc = 0x0020_0010; - let arm7_frames = (0..16) .map(|index| CallstackFrame { index, @@ -978,17 +881,13 @@ impl ServerState { let timestamp = current_timestamp_ms(); - #[cfg(not(target_arch = "wasm32"))] let is_running = if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; - crate::dreamcast::is_dreamcast_running(dreamcast) + let dreamcast = dreamcast_ptr as *mut dreamcast::Dreamcast; + dreamcast::is_dreamcast_running(dreamcast) } else { false // Default to paused when no emulator context }; - #[cfg(target_arch = "wasm32")] - let is_running = false; // Default to paused for WASM - DebuggerTick { tick_id, timestamp, @@ -1009,97 +908,6 @@ impl ServerState { } } -fn sha256_byte(input: &str) -> u8 { - let mut hasher = Sha256::new(); - hasher.update(input.as_bytes()); - let result = hasher.finalize(); - result[0] -} - -fn generate_disassembly(target: &str, address: u64, count: usize) -> Vec { - type OperandFn = fn(u8, u8, u8, u8, u16) -> String; - - let sh4_instructions: Vec<(&str, OperandFn, u64)> = vec![ - ("mov.l", |r1, r2, _, _, _| format!("@r{}+, r{}", r1, r2), 2), - ("mov", |r1, r2, _, _, _| format!("r{}, r{}", r1, r2), 2), - ("sts.l", |r1, _, _, _, _| format!("pr, @-r{}", r1), 2), - ("add", |r1, r2, _, _, _| format!("r{}, r{}", r1, r2), 2), - ("cmp/eq", |r1, r2, _, _, _| format!("r{}, r{}", r1, r2), 2), - ("bf", |_, _, _, _, offset| format!("0x{:x}", offset), 2), - ("jmp", |r, _, _, _, _| format!("@r{}", r), 2), - ("nop", |_, _, _, _, _| String::new(), 2), - ]; - - let arm7_instructions: Vec<(&str, OperandFn, u64)> = vec![ - ("mov", |r1, _, _, val, _| format!("r{}, #{}", r1, val), 4), - ( - "ldr", - |r1, r2, _, _, offset| format!("r{}, [r{}, #{}]", r1, r2, offset), - 4, - ), - ("str", |r1, r2, _, _, _| format!("r{}, [r{}]", r1, r2), 4), - ( - "add", - |r1, r2, r3, _, _| format!("r{}, r{}, r{}", r1, r2, r3), - 4, - ), - ( - "sub", - |r1, r2, r3, _, _| format!("r{}, r{}, r{}", r1, r2, r3), - 4, - ), - ("bx", |r, _, _, _, _| format!("r{}", r), 4), - ("bl", |_, _, _, _, offset| format!("0x{:x}", offset), 4), - ("nop", |_, _, _, _, _| String::new(), 4), - ]; - - let selected = if target == "arm7" { - &arm7_instructions - } else { - &sh4_instructions - }; - - let mut lines = Vec::new(); - let mut current_addr = address; - - for _ in 0..count { - let hash = sha256_byte(&format!("{}:{:x}", target, current_addr)); - let instr_index = (hash as usize) % selected.len(); - let (mnemonic, operand_fn, bytes_len) = selected[instr_index]; - - let r1 = (hash >> 4) % 16; - let r2 = (hash >> 2) % 16; - let r3 = hash % 16; - let val = (hash.wrapping_mul(3)) & 0xff; - let offset = (hash.wrapping_mul(7) as u16) & 0xfff; - - let operands = operand_fn(r1, r2, r3, val, offset); - let disassembly = if operands.is_empty() { - mnemonic.to_string() - } else { - format!("{} {}", mnemonic, operands) - }; - - let byte_values: Vec = (0..bytes_len) - .map(|b| { - format!( - "{:02X}", - sha256_byte(&format!("{}:{:x}:{}", target, current_addr, b)) - ) - }) - .collect(); - - lines.push(DisassemblyLine { - address: current_addr, - bytes: byte_values.join(" "), - disassembly, - }); - - current_addr += bytes_len; - } - - lines -} fn build_memory_slice( dreamcast_ptr: usize, @@ -1117,32 +925,24 @@ fn build_memory_slice( let base_address = address.unwrap_or(default_base); let effective_length = length.unwrap_or(64); - // Try to read from actual emulator memory if pointer is valid - #[cfg(not(target_arch = "wasm32"))] let bytes: Vec = if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; + let dreamcast = dreamcast_ptr as *mut dreamcast::Dreamcast; match target { "arm7" => { - crate::dreamcast::read_arm_memory_slice(dreamcast, base_address, effective_length) + dreamcast::read_arm_memory_slice(dreamcast, base_address, effective_length) } - _ => crate::dreamcast::read_memory_slice(dreamcast, base_address, effective_length), + _ => dreamcast::read_memory_slice(dreamcast, base_address, effective_length), } } else { - // Fall back to mock data if no emulator context - (0..effective_length) - .map(|i| sha256_byte(&format!("{}:{:x}", target, base_address + i as u64))) - .collect() + Vec::new() }; - #[cfg(target_arch = "wasm32")] - let bytes: Vec = (0..effective_length) - .map(|i| sha256_byte(&format!("{}:{:x}", target, base_address + i as u64))) - .collect(); - + let bytes_empty = bytes.is_empty(); + MemorySlice { base_address, data: bytes, - validity: "ok".to_string(), + validity: if bytes_empty { "unavailable".to_string() } else { "ok".to_string() }, } } @@ -1213,11 +1013,10 @@ pub fn handle_request( .and_then(|value| value.as_u64()) .unwrap_or(128) as usize; - #[cfg(not(target_arch = "wasm32"))] let lines = if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; + let dreamcast = dreamcast_ptr as *mut dreamcast::Dreamcast; match target { - "sh4" => crate::dreamcast::disassemble_sh4(dreamcast, address, count) + "sh4" => dreamcast::disassemble_sh4(dreamcast, address, count) .into_iter() .map(|line| DisassemblyLine { address: line.address, @@ -1225,7 +1024,7 @@ pub fn handle_request( disassembly: line.disassembly, }) .collect::>(), - "arm7" => crate::dreamcast::disassemble_arm7(dreamcast, address, count) + "arm7" => dreamcast::disassemble_arm7(dreamcast, address, count) .into_iter() .map(|line| DisassemblyLine { address: line.address, @@ -1233,15 +1032,12 @@ pub fn handle_request( disassembly: line.disassembly, }) .collect::>(), - _ => generate_disassembly(target, address, count), + _ => Vec::new(), // Unsupported target, return empty } } else { - generate_disassembly(target, address, count) + Vec::new() }; - #[cfg(target_arch = "wasm32")] - let lines = generate_disassembly(target, address, count); - Ok((json!({ "lines": lines }), false)) } @@ -1370,29 +1166,26 @@ pub fn handle_request( } "control.pause" => { - #[cfg(not(target_arch = "wasm32"))] if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; - crate::dreamcast::set_dreamcast_running(dreamcast, false); + let dreamcast = dreamcast_ptr as *mut dreamcast::Dreamcast; + dreamcast::set_dreamcast_running(dreamcast, false); } Ok((json!({}), true)) } "control.step" | "control.stepOver" | "control.stepOut" => { - #[cfg(not(target_arch = "wasm32"))] if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; - crate::dreamcast::step_dreamcast(dreamcast); - crate::dreamcast::set_dreamcast_running(dreamcast, false); + let dreamcast = dreamcast_ptr as *mut dreamcast::Dreamcast; + dreamcast::step_dreamcast(dreamcast); + dreamcast::set_dreamcast_running(dreamcast, false); } Ok((json!({}), true)) } "control.runUntil" => { - #[cfg(not(target_arch = "wasm32"))] if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut crate::dreamcast::Dreamcast; - crate::dreamcast::set_dreamcast_running(dreamcast, true); + let dreamcast = dreamcast_ptr as *mut dreamcast::Dreamcast; + dreamcast::set_dreamcast_running(dreamcast, true); } Ok((json!({}), true)) } From a0a8ed9f3cf07280a0406827672e2d50af316eee Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Tue, 14 Oct 2025 18:59:24 +0300 Subject: [PATCH 06/15] ci: fix devtools --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e002103..ca7e730 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -46,7 +46,7 @@ jobs: working-directory: ./devtools run: | npm ci - npm run build + npm run build:${{ matrix.variant }} env: VITE_TRANSPORT: ${{ matrix.variant == 'wasm' && 'broadcast' || 'websocket' }} From a994ed41aaadaeae82d13422f224205cde54ae84 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Tue, 14 Oct 2025 20:14:20 +0300 Subject: [PATCH 07/15] build: re-strip release bins for cloudflare limit --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index d813d97..117915a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,10 @@ opt-level = 3 strip = false debug = true +[profile.release.package."*"] +debug = false +strip = true + [features] default = ["refsw2-rust"] refsw2-rust = ["dreamcast/refsw2-rust"] From 3f22adf4b28775311037af504fac3ff1d2240b4f Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Tue, 14 Oct 2025 20:47:56 +0300 Subject: [PATCH 08/15] emu: fb texture should not be sRGB --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b8be4dc..0164759 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -158,7 +158,7 @@ impl State { mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, + format: wgpu::TextureFormat::Rgba8Unorm, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, label: Some("framebuffer_texture"), view_formats: &[], @@ -382,7 +382,7 @@ impl State { mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, + format: wgpu::TextureFormat::Rgba8Unorm, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, label: Some("framebuffer_texture"), view_formats: &[], From 214613263ea1b7d1f56c6bcfde611d9d04dbf25d Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Tue, 14 Oct 2025 22:07:24 +0300 Subject: [PATCH 09/15] native ui: add devtools menu button --- Cargo.lock | 672 +++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 4 + src/lib.rs | 67 +++++- 3 files changed, 718 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 435ee75..478468c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -168,6 +168,29 @@ dependencies = [ "libloading", ] +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -339,7 +362,7 @@ checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -348,6 +371,31 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.9.4", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "calloop" version = "0.13.0" @@ -392,6 +440,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.3" @@ -520,6 +578,24 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -657,12 +733,41 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset 0.9.1", + "rustc_version", +] + [[package]] name = "find-msvc-tools" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +[[package]] +name = "flate2" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -693,7 +798,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -767,7 +872,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -800,6 +905,64 @@ dependencies = [ "slab", ] +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -838,6 +1001,38 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + [[package]] name = "git-version" version = "0.3.9" @@ -855,7 +1050,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -869,6 +1064,53 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.9.4", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "glow" version = "0.16.0" @@ -890,6 +1132,17 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "gpu-alloc" version = "0.6.0" @@ -941,6 +1194,58 @@ dependencies = [ "bitflags 2.9.4", ] +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "half" version = "2.6.0" @@ -967,6 +1272,18 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.5.2" @@ -1120,6 +1437,25 @@ dependencies = [ "libc", ] +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1153,7 +1489,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -1198,6 +1534,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.9.4", + "serde", + "unicode-segmentation", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -1254,6 +1601,25 @@ dependencies = [ "redox_syscall 0.5.17", ] +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +dependencies = [ + "libc", + "x11", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1327,6 +1693,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "metal" version = "0.32.0" @@ -1391,6 +1766,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1404,6 +1780,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "muda" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "libxdo", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "png", + "thiserror 1.0.69", + "windows-sys 0.59.0", +] + [[package]] name = "naga" version = "26.0.0" @@ -1469,7 +1865,7 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -1486,6 +1882,8 @@ dependencies = [ "include_dir", "js-sys", "log", + "muda", + "open", "pollster", "serde", "serde_json", @@ -1536,10 +1934,10 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -1775,6 +2173,17 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "orbclient" version = "0.3.48" @@ -1805,6 +2214,31 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "parking_lot" version = "0.12.4" @@ -1834,6 +2268,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1857,7 +2297,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -1878,6 +2318,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -1928,13 +2381,57 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.6", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] @@ -2096,6 +2593,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -2188,9 +2694,15 @@ checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0" dependencies = [ "cfg-if", "libc", - "version-compare", + "version-compare 0.1.1", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "seq-macro" version = "0.3.6" @@ -2224,7 +2736,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -2251,6 +2763,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2311,6 +2832,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.11" @@ -2397,6 +2924,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.106" @@ -2414,6 +2951,25 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare 0.2.0", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.23.0" @@ -2462,7 +3018,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -2473,7 +3029,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -2529,7 +3085,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -2557,6 +3113,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.2" @@ -2566,6 +3143,30 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.23.6" @@ -2573,9 +3174,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 0.7.2", "toml_parser", - "winnow", + "winnow 0.7.13", ] [[package]] @@ -2584,7 +3185,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" dependencies = [ - "winnow", + "winnow 0.7.13", ] [[package]] @@ -2753,6 +3354,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.5" @@ -2816,7 +3423,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -2851,7 +3458,7 @@ checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3289,7 +3896,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -3300,7 +3907,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -3611,6 +4218,15 @@ dependencies = [ "xkbcommon-dl", ] +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.7.13" @@ -3626,6 +4242,16 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -3706,5 +4332,5 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] diff --git a/Cargo.toml b/Cargo.toml index 117915a..ef3733a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,10 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" uuid = { version = "1.0", features = ["v4"] } +# Menu support +muda = "0.15" +open = "5.0" + [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" diff --git a/src/lib.rs b/src/lib.rs index 0164759..6202341 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,9 @@ use wasm_logger; #[cfg(target_arch = "wasm32")] use wgpu::web_sys; +#[cfg(not(target_arch = "wasm32"))] +use muda::{Menu, MenuItem}; + use winit::window::Window; use winit::window::WindowAttributes; use winit::{ @@ -445,10 +448,26 @@ impl State { } } -#[derive(Default)] struct App { state: Option, dreamcast: *mut Dreamcast, + #[cfg(not(target_arch = "wasm32"))] + menu: Option, + #[cfg(not(target_arch = "wasm32"))] + devtools_item: Option, +} + +impl Default for App { + fn default() -> Self { + Self { + state: None, + dreamcast: std::ptr::null_mut(), + #[cfg(not(target_arch = "wasm32"))] + menu: None, + #[cfg(not(target_arch = "wasm32"))] + devtools_item: None, + } + } } // Import ApplicationHandler trait from winit @@ -464,6 +483,10 @@ impl AppHandle { AppHandle(Rc::new(RefCell::new(App { state: None, dreamcast: dreamcast, + #[cfg(not(target_arch = "wasm32"))] + menu: None, + #[cfg(not(target_arch = "wasm32"))] + devtools_item: None, }))) } } @@ -512,8 +535,30 @@ impl ApplicationHandler for AppHandle { #[cfg(not(target_arch = "wasm32"))] { + // Create menu + let menu = Menu::new(); + let devtools_item = MenuItem::new("DevTools", true, None); + + menu.append(&devtools_item).unwrap(); + + // Initialize menu for the window + #[cfg(target_os = "windows")] + { + use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle}; + if let Ok(handle) = window.window_handle() { + if let RawWindowHandle::Win32(win32_handle) = handle.as_ref() { + unsafe { + menu.init_for_hwnd(win32_handle.hwnd.get() as _).unwrap(); + } + } + } + } + let state = pollster::block_on(State::new(window)); - self.0.borrow_mut().state = Some(state); + let mut app = self.0.borrow_mut(); + app.state = Some(state); + app.menu = Some(menu); + app.devtools_item = Some(devtools_item); } #[cfg(target_arch = "wasm32")] @@ -607,6 +652,24 @@ impl ApplicationHandler for AppHandle { fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { let app = self.0.borrow_mut(); + + // Handle menu events (non-WASM only) + #[cfg(not(target_arch = "wasm32"))] + { + if let Some(ref devtools_item) = app.devtools_item { + if let Ok(event) = muda::MenuEvent::receiver().try_recv() { + if event.id == devtools_item.id() { + // Open DevTools URL in default browser + if let Err(e) = open::that("http://127.0.0.1:55543") { + log::error!("Failed to open DevTools URL: {}", e); + } else { + log::info!("Opened DevTools in default browser"); + } + } + } + } + } + if let Some(state) = app.state.as_ref() { state.window.request_redraw(); } From 29e0279a0de49acfb087a69266b20bbea98e3137 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Tue, 14 Oct 2025 22:07:38 +0300 Subject: [PATCH 10/15] presentation: Allow only integer multiplies of FB --- src/shader.wgsl | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/shader.wgsl b/src/shader.wgsl index 4e183f6..8ffb4a1 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -33,31 +33,30 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { let pos = positions[vertex_index]; - // Convert from 0-1 range to -1 to 1 clip space - let x = pos.x * 2.0 - 1.0; - let y = pos.y * 2.0 - 1.0; + // Integer-only scaling when window is large enough + var imgsize_x = uniforms.window_width; + var imgsize_y = uniforms.window_height; + var topleft_x = 0.0; + var topleft_y = 0.0; - // Calculate aspect ratios - let window_aspect = uniforms.window_width / uniforms.window_height; - let fb_aspect = uniforms.fb_width / uniforms.fb_height; // 4:3 = 1.333... - - // Calculate scale to maintain 4:3 aspect ratio - var scale_x = 1.0; - var scale_y = 1.0; - - if (window_aspect > fb_aspect) { - // Window is wider than 4:3 - add pillarboxing (black bars on sides) - scale_x = fb_aspect / window_aspect; - } else { - // Window is taller than 4:3 - add letterboxing (black bars top/bottom) - scale_y = window_aspect / fb_aspect; + if (uniforms.window_width > uniforms.fb_width && uniforms.window_height > uniforms.fb_height) { + // Calculate integer scale factor + let scale = floor(min(uniforms.window_width / uniforms.fb_width, uniforms.window_height / uniforms.fb_height)); + imgsize_x = uniforms.fb_width * scale; + imgsize_y = uniforms.fb_height * scale; + topleft_x = floor((uniforms.window_width - imgsize_x) / 2.0); + topleft_y = floor((uniforms.window_height - imgsize_y) / 2.0); } - // Apply scaling to maintain aspect ratio - let scaled_x = x * scale_x; - let scaled_y = y * scale_y; + // Convert to normalized device coordinates (-1 to 1) + // Map from pixel coordinates to NDC + let pixel_x = topleft_x + pos.x * imgsize_x; + let pixel_y = topleft_y + pos.y * imgsize_y; + + let ndc_x = (pixel_x / uniforms.window_width) * 2.0 - 1.0; + let ndc_y = (pixel_y / uniforms.window_height) * 2.0 - 1.0; - out.clip_position = vec4(scaled_x, scaled_y, 0.0, 1.0); + out.clip_position = vec4(ndc_x, ndc_y, 0.0, 1.0); // Texture coordinates (flip Y for correct orientation) out.tex_coords = vec2(pos.x, 1.0 - pos.y); From 1d4b1f54df966bd5dcc9d60bcbc66ddb90063bfc Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Tue, 14 Oct 2025 22:09:07 +0300 Subject: [PATCH 11/15] presentation: make framebuffer non srgb --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 6202341..1235cbb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,7 +128,7 @@ impl State { .formats .iter() .copied() - .find(|f| f.is_srgb()) + .find(|f| !f.is_srgb()) .unwrap_or(surface_caps.formats[0]); let present_mode = surface_caps From 59d1654421cef52533cd3e2da7ce9be1a2faef6a Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Tue, 14 Oct 2025 22:24:49 +0300 Subject: [PATCH 12/15] emu: rename debugger servers --- ....rs => debugger_html5_broadcast_server.rs} | 0 ...server.rs => debugger_websocket_server.rs} | 0 src/lib.rs | 8 +- src/mock_debug_server.rs | 1712 ----------------- 4 files changed, 4 insertions(+), 1716 deletions(-) rename src/{broadcast_debug_server.rs => debugger_html5_broadcast_server.rs} (100%) rename src/{websocket_debug_server.rs => debugger_websocket_server.rs} (100%) delete mode 100644 src/mock_debug_server.rs diff --git a/src/broadcast_debug_server.rs b/src/debugger_html5_broadcast_server.rs similarity index 100% rename from src/broadcast_debug_server.rs rename to src/debugger_html5_broadcast_server.rs diff --git a/src/websocket_debug_server.rs b/src/debugger_websocket_server.rs similarity index 100% rename from src/websocket_debug_server.rs rename to src/debugger_websocket_server.rs diff --git a/src/lib.rs b/src/lib.rs index 1235cbb..9febd08 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,14 +30,14 @@ use dreamcast::{Dreamcast, present_for_texture, run_slice_dreamcast}; mod debugger_core; #[cfg(target_arch = "wasm32")] -mod broadcast_debug_server; +mod debugger_html5_broadcast_server; #[cfg(target_arch = "wasm32")] -use broadcast_debug_server::BroadcastDebugServer; +use debugger_html5_broadcast_server::BroadcastDebugServer; #[cfg(not(target_arch = "wasm32"))] -mod websocket_debug_server; +mod debugger_websocket_server; #[cfg(not(target_arch = "wasm32"))] -pub use websocket_debug_server::start_debugger_server; +pub use debugger_websocket_server::start_debugger_server; const GIT_HASH: &str = git_version!(); diff --git a/src/mock_debug_server.rs b/src/mock_debug_server.rs deleted file mode 100644 index 6f0adfe..0000000 --- a/src/mock_debug_server.rs +++ /dev/null @@ -1,1712 +0,0 @@ -use axum::extract::ws::{Message, WebSocket}; -use futures::SinkExt; -use futures::stream::StreamExt; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use sha2::{Digest, Sha256}; -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; -use std::time::{SystemTime, UNIX_EPOCH}; - -const JSON_RPC_VERSION: &str = "2.0"; - -#[allow(dead_code)] -mod panel_ids { - pub const DOCUMENTATION: &str = "documentation"; - pub const SH4_SIM: &str = "sh4-sim"; - pub const EVENTS: &str = "events"; - pub const EVENTS_BREAKPOINTS: &str = "events-breakpoints"; - pub const SH4_DISASSEMBLY: &str = "sh4-disassembly"; - pub const SH4_MEMORY: &str = "sh4-memory"; - pub const SH4_BREAKPOINTS: &str = "sh4-breakpoints"; - pub const SH4_BSC_REGISTERS: &str = "bsc-registers"; - pub const SH4_CCN_REGISTERS: &str = "ccn-registers"; - pub const SH4_CPG_REGISTERS: &str = "cpg-registers"; - pub const SH4_DMAC_REGISTERS: &str = "dmac-registers"; - pub const SH4_INTC_REGISTERS: &str = "intc-registers"; - pub const SH4_RTC_REGISTERS: &str = "rtc-registers"; - pub const SH4_SCI_REGISTERS: &str = "sci-registers"; - pub const SH4_SCIF_REGISTERS: &str = "scif-registers"; - pub const SH4_TMU_REGISTERS: &str = "tmu-registers"; - pub const SH4_UBC_REGISTERS: &str = "ubc-registers"; - pub const SH4_SQ_CONTENTS: &str = "sq-contents"; - pub const SH4_ICACHE_CONTENTS: &str = "icache-contents"; - pub const SH4_OCACHE_CONTENTS: &str = "ocache-contents"; - pub const SH4_OCRAM_CONTENTS: &str = "ocram-contents"; - pub const SH4_TLB_CONTENTS: &str = "tlb-contents"; - pub const ARM7_DISASSEMBLY: &str = "arm7-disassembly"; - pub const ARM7_MEMORY: &str = "arm7-memory"; - pub const ARM7_BREAKPOINTS: &str = "arm7-breakpoints"; - pub const CLX2_TA: &str = "holly-ta"; - pub const CLX2_CORE: &str = "holly-core"; - pub const SGC: &str = "sgc"; - pub const DSP_DISASSEMBLY: &str = "dsp-disassembly"; - pub const DSP_BREAKPOINTS: &str = "dsp-breakpoints"; - pub const DSP_PLAYGROUND: &str = "dsp-playground"; - pub const DEVICE_TREE: &str = "device-tree"; - pub const WATCHES: &str = "watches"; - pub const SH4_CALLSTACK: &str = "sh4-callstack"; - pub const ARM7_CALLSTACK: &str = "arm7-callstack"; -} - -const DEFAULT_WATCH_EXPRESSIONS: &[&str] = &["dc.sh4.cpu.pc", "dc.sh4.dmac.dmaor"]; -const EVENT_LOG_LIMIT: usize = 60; -const CAPABILITIES: &[&str] = &["watches", "step", "breakpoints", "frame-log"]; - -type FrameEventGenerator = fn(u64) -> (&'static str, &'static str, String); - -fn frame_event_ta(counter: u64) -> (&'static str, &'static str, String) { - ( - "ta", - "info", - format!("TA/END_LIST tile {}", (counter % 32) as usize), - ) -} - -fn frame_event_core(counter: u64) -> (&'static str, &'static str, String) { - let phase = match counter % 3 { - 0 => "START_RENDER", - 1 => "QUEUE_SUBMISSION", - _ => "END_RENDER", - }; - ( - "core", - if phase == "QUEUE_SUBMISSION" { - "trace" - } else { - "info" - }, - format!("CORE/{}", phase), - ) -} - -fn frame_event_dsp(counter: u64) -> (&'static str, &'static str, String) { - ( - "dsp", - "trace", - format!("DSP/STEP pipeline advanced ({})", counter % 8), - ) -} - -fn frame_event_aica(counter: u64) -> (&'static str, &'static str, String) { - ( - "aica", - "info", - format!("AICA/SGC/STEP channel {}", (counter % 2) as usize), - ) -} - -fn frame_event_sh4(counter: u64) -> (&'static str, &'static str, String) { - ( - "sh4", - "warn", - format!("SH4/INTERRUPT IRQ{} asserted", (counter % 6) + 1), - ) -} - -fn frame_event_holly(counter: u64) -> (&'static str, &'static str, String) { - ( - "holly", - "info", - format!("HOLLY/START_RENDER pass {}", (counter % 4) + 1), - ) -} - -const FRAME_EVENT_GENERATORS: &[FrameEventGenerator] = &[ - frame_event_ta, - frame_event_core, - frame_event_dsp, - frame_event_aica, - frame_event_sh4, - frame_event_holly, -]; - -fn create_frame_event_with_id(event_id: u64) -> EventLogEntry { - let generator = FRAME_EVENT_GENERATORS[(event_id as usize) % FRAME_EVENT_GENERATORS.len()]; - let (subsystem, severity, message) = generator(event_id); - EventLogEntry { - event_id: event_id.to_string(), - timestamp: current_timestamp_ms(), - subsystem: subsystem.to_string(), - severity: severity.to_string(), - message, - metadata: None, - } -} - -fn initial_event_log() -> (Vec, u64) { - let mut log = Vec::new(); - for id in 1..=6 { - log.push(create_frame_event_with_id(id)); - } - (log, 7) -} - -fn current_timestamp_ms() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_else(|_| std::time::Duration::from_millis(0)) - .as_millis() as u64 -} - -// JSON-RPC structures -#[derive(Debug, Serialize, Deserialize)] -pub struct JsonRpcRequest { - pub jsonrpc: String, - pub id: serde_json::Value, - pub method: String, - pub params: Option, -} - -#[derive(Debug, Serialize)] -pub struct JsonRpcSuccess { - pub jsonrpc: String, - pub id: serde_json::Value, - pub result: serde_json::Value, -} - -#[derive(Debug, Serialize)] -pub struct JsonRpcError { - pub jsonrpc: String, - pub id: serde_json::Value, - pub error: JsonRpcErrorObject, -} - -#[derive(Debug, Serialize)] -pub struct JsonRpcErrorObject { - pub code: i32, - pub message: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, -} - -#[derive(Debug, Serialize)] -pub struct JsonRpcNotification { - pub jsonrpc: String, - pub method: String, - pub params: serde_json::Value, -} - -// Debugger schema structures -#[derive(Debug, Clone, Serialize, Deserialize)] -struct RegisterValue { - name: String, - value: String, - width: u32, - #[serde(skip_serializing_if = "Option::is_none")] - flags: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - metadata: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct DeviceNodeDescriptor { - path: String, - label: String, - #[serde(skip_serializing_if = "Option::is_none")] - description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - registers: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - events: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - actions: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - children: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct BreakpointDescriptor { - id: u32, - event: String, - #[serde(skip_serializing_if = "Option::is_none")] - address: Option, - kind: String, // "code" or "event" - enabled: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct CallstackFrame { - index: u32, - pc: u64, - #[serde(skip_serializing_if = "Option::is_none")] - sp: Option, - #[serde(skip_serializing_if = "Option::is_none")] - symbol: Option, - #[serde(skip_serializing_if = "Option::is_none")] - location: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct DisassemblyLine { - address: u64, - bytes: String, - disassembly: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct MemorySlice { - #[serde(rename = "baseAddress")] - base_address: u64, - data: Vec, - validity: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct EventLogEntry { - #[serde(rename = "eventId")] - event_id: String, - timestamp: u64, - subsystem: String, - severity: String, - message: String, - #[serde(skip_serializing_if = "Option::is_none")] - metadata: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct WatchDescriptor { - id: u32, - expression: String, - value: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct DebuggerTick { - #[serde(rename = "tickId")] - tick_id: u64, - timestamp: u64, - #[serde(rename = "executionState")] - execution_state: ExecutionState, - registers: HashMap>, - breakpoints: HashMap, - #[serde(rename = "eventLog")] - event_log: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - watches: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - callstacks: Option>>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ExecutionState { - state: String, // "running" or "paused" - #[serde(rename = "breakpointId", skip_serializing_if = "Option::is_none")] - breakpoint_id: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct BreakpointCategoryState { - muted: bool, - soloed: bool, -} - -// Server watch structure -#[derive(Debug, Clone)] -struct ServerWatch { - id: u32, - expression: String, -} - -// Server state -pub struct ServerState { - breakpoints: Arc>>, - watches: Arc>>, - register_values: Arc>>, - event_log: Arc>>, - category_states: Arc>>, - is_running: Arc>, - tick_id: Arc>, - next_event_id: Arc>, - next_watch_id: Arc>, - next_breakpoint_id: Arc>, -} - -impl ServerState { - pub fn new() -> Self { - let mut register_values = HashMap::new(); - - // Initialize register values - register_values.insert("dc.sh4.cpu.pc".to_string(), "0x8C0000A0".to_string()); - register_values.insert("dc.sh4.cpu.pr".to_string(), "0x8C0000A2".to_string()); - register_values.insert("dc.sh4.vbr".to_string(), "0x8C000000".to_string()); - register_values.insert("dc.sh4.sr".to_string(), "0x40000000".to_string()); - register_values.insert("dc.sh4.fpscr".to_string(), "0x00040001".to_string()); - register_values.insert("dc.sh4.cpu.gbr".to_string(), "0x8C000100".to_string()); - register_values.insert("dc.sh4.cpu.mach".to_string(), "0x00000000".to_string()); - register_values.insert("dc.sh4.cpu.macl".to_string(), "0x00000000".to_string()); - register_values.insert("dc.sh4.cpu.fpul".to_string(), "0x00000000".to_string()); - register_values.insert( - "dc.sh4.icache.icache_ctrl".to_string(), - "0x00000003".to_string(), - ); - register_values.insert( - "dc.sh4.dcache.dcache_ctrl".to_string(), - "0x00000003".to_string(), - ); - register_values.insert("dc.sh4.dmac.dmaor".to_string(), "0x8201".to_string()); - register_values.insert("dc.holly.holly_id".to_string(), "0x00050000".to_string()); - register_values.insert("dc.holly.dmac_ctrl".to_string(), "0x00000001".to_string()); - register_values.insert("dc.holly.dmac.dmaor".to_string(), "0x8201".to_string()); - register_values.insert("dc.holly.dmac.chcr0".to_string(), "0x00000001".to_string()); - register_values.insert( - "dc.holly.ta.ta_list_base".to_string(), - "0x0C000000".to_string(), - ); - register_values.insert( - "dc.holly.ta.ta_status".to_string(), - "0x00000000".to_string(), - ); - register_values.insert( - "dc.holly.core.pvr_ctrl".to_string(), - "0x00000001".to_string(), - ); - register_values.insert( - "dc.holly.core.pvr_status".to_string(), - "0x00010000".to_string(), - ); - register_values.insert("dc.aica.aica_ctrl".to_string(), "0x00000002".to_string()); - register_values.insert("dc.aica.aica_status".to_string(), "0x00000001".to_string()); - register_values.insert("dc.aica.arm7.pc".to_string(), "0x00200010".to_string()); - register_values.insert("dc.aica.channels.ch0_vol".to_string(), "0x7F".to_string()); - register_values.insert("dc.aica.channels.ch1_vol".to_string(), "0x6A".to_string()); - register_values.insert("dc.aica.dsp.step".to_string(), "0x000".to_string()); - register_values.insert("dc.aica.dsp.dsp_acc".to_string(), "0x1F".to_string()); - register_values.insert("dc.sysclk".to_string(), "200MHz".to_string()); - register_values.insert("dc.asic_rev".to_string(), "0x0001".to_string()); - - // Initialize default watches - let mut watches = HashMap::new(); - let mut next_watch_id = 1; - for expr in DEFAULT_WATCH_EXPRESSIONS { - watches.insert( - next_watch_id, - ServerWatch { - id: next_watch_id, - expression: expr.to_string(), - }, - ); - next_watch_id += 1; - } - - // Initialize category states - let mut category_states = HashMap::new(); - for category in &["events", "sh4", "arm7", "dsp"] { - category_states.insert( - category.to_string(), - BreakpointCategoryState { - muted: false, - soloed: false, - }, - ); - } - - // Initialize event log with sample entries - let (event_log, next_event_id) = initial_event_log(); - - Self { - breakpoints: Arc::new(Mutex::new(HashMap::new())), - watches: Arc::new(Mutex::new(watches)), - register_values: Arc::new(Mutex::new(register_values)), - event_log: Arc::new(Mutex::new(event_log)), - category_states: Arc::new(Mutex::new(category_states)), - is_running: Arc::new(Mutex::new(true)), - tick_id: Arc::new(Mutex::new(0)), - next_event_id: Arc::new(Mutex::new(next_event_id)), - next_watch_id: Arc::new(Mutex::new(next_watch_id)), - next_breakpoint_id: Arc::new(Mutex::new(1)), - } - } - - fn get_register_value(&self, path: &str, name: &str) -> String { - let key = format!("{}.{}", path, name.to_lowercase()); - self.register_values - .lock() - .unwrap() - .get(&key) - .cloned() - .unwrap_or_else(|| "0x00000000".to_string()) - } - - fn set_register_value(&self, path: &str, name: &str, value: String) { - let key = format!("{}.{}", path, name.to_lowercase()); - self.register_values.lock().unwrap().insert(key, value); - } - - fn evaluate_watch_expression(&self, dreamcast_ptr: usize, expression: &str) -> String { - // Expression format: "dc.sh4.cpu.PC" or just "PC" (defaults to dc.sh4.cpu) - // Split into path and register name - let parts: Vec<&str> = expression.split('.').collect(); - - if parts.is_empty() { - return "0x00000000".to_string(); - } - - // If expression is a full path like "dc.sh4.cpu.R0" - let (path, name) = if parts.len() > 1 { - let name = parts.last().unwrap(); - let path = parts[..parts.len() - 1].join("."); - (path, name.to_string()) - } else { - // Default to dc.sh4.cpu if just register name - ("dc.sh4.cpu".to_string(), parts[0].to_string()) - }; - - // Try to get value from actual emulator if available - if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut nulldc::dreamcast::Dreamcast; - if path == "dc.sh4.cpu" { - if let Some(value) = nulldc::dreamcast::get_sh4_register(dreamcast, &name) { - return format!("0x{:08X}", value); - } - } else if path == "dc.aica.arm7" { - if let Some(value) = nulldc::dreamcast::get_arm_register(dreamcast, &name) { - return format!("0x{:08X}", value); - } - } - } - - // Fall back to mock values - self.get_register_value(&path, &name) - } - - fn build_device_tree(&self) -> Vec { - let register = |path: &str, name: &str, width: u32| RegisterValue { - name: name.to_string(), - value: self.get_register_value(path, name), - width, - flags: None, - metadata: None, - }; - - let mut sh4_core_registers = vec![ - register("dc.sh4.cpu", "PC", 32), - register("dc.sh4.cpu", "PR", 32), - register("dc.sh4", "VBR", 32), - register("dc.sh4", "SR", 32), - register("dc.sh4", "FPSCR", 32), - register("dc.sh4.cpu", "GBR", 32), - register("dc.sh4.cpu", "MACH", 32), - register("dc.sh4.cpu", "MACL", 32), - register("dc.sh4.cpu", "FPUL", 32), - ]; - for idx in 0..16 { - sh4_core_registers.push(register("dc.sh4.cpu", &format!("R{}", idx), 32)); - } - - let sh4_pbus_children = vec![ - DeviceNodeDescriptor { - path: "dc.sh4.bsc".to_string(), - label: "BSC".to_string(), - description: Some("Bus State Controller".to_string()), - registers: None, - events: None, - actions: Some(vec![panel_ids::SH4_BSC_REGISTERS.to_string()]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.sh4.ccn".to_string(), - label: "CCN".to_string(), - description: Some("Cache Controller".to_string()), - registers: None, - events: None, - actions: Some(vec![panel_ids::SH4_CCN_REGISTERS.to_string()]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.sh4.cpg".to_string(), - label: "CPG".to_string(), - description: Some("Clock Pulse Generator".to_string()), - registers: None, - events: None, - actions: Some(vec![panel_ids::SH4_CPG_REGISTERS.to_string()]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.sh4.dmac".to_string(), - label: "DMAC".to_string(), - description: Some("Direct Memory Access Controller".to_string()), - registers: None, - events: None, - actions: Some(vec![panel_ids::SH4_DMAC_REGISTERS.to_string()]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.sh4.intc".to_string(), - label: "INTC".to_string(), - description: Some("Interrupt Controller".to_string()), - registers: None, - events: None, - actions: Some(vec![panel_ids::SH4_INTC_REGISTERS.to_string()]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.sh4.rtc".to_string(), - label: "RTC".to_string(), - description: Some("Real Time Clock".to_string()), - registers: None, - events: None, - actions: Some(vec![panel_ids::SH4_RTC_REGISTERS.to_string()]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.sh4.sci".to_string(), - label: "SCI".to_string(), - description: Some("Serial Communications Interface".to_string()), - registers: None, - events: None, - actions: Some(vec![panel_ids::SH4_SCI_REGISTERS.to_string()]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.sh4.scif".to_string(), - label: "SCIF".to_string(), - description: Some("Serial Communications Interface w/ FIFO".to_string()), - registers: None, - events: None, - actions: Some(vec![panel_ids::SH4_SCIF_REGISTERS.to_string()]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.sh4.tmu".to_string(), - label: "TMU".to_string(), - description: Some("Timer Unit".to_string()), - registers: None, - events: None, - actions: Some(vec![panel_ids::SH4_TMU_REGISTERS.to_string()]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.sh4.ubc".to_string(), - label: "UBC".to_string(), - description: Some("User Break Controller".to_string()), - registers: None, - events: None, - actions: Some(vec![panel_ids::SH4_UBC_REGISTERS.to_string()]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.sh4.sq".to_string(), - label: "SQ".to_string(), - description: Some("Store Queues".to_string()), - registers: None, - events: None, - actions: Some(vec![panel_ids::SH4_SQ_CONTENTS.to_string()]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.sh4.icache".to_string(), - label: "ICACHE".to_string(), - description: Some("Instruction Cache".to_string()), - registers: None, - events: None, - actions: Some(vec![panel_ids::SH4_ICACHE_CONTENTS.to_string()]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.sh4.ocache".to_string(), - label: "OCACHE".to_string(), - description: Some("Operand Cache".to_string()), - registers: None, - events: None, - actions: Some(vec![panel_ids::SH4_OCACHE_CONTENTS.to_string()]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.sh4.ocram".to_string(), - label: "OCRAM".to_string(), - description: Some("Operand RAM".to_string()), - registers: None, - events: None, - actions: Some(vec![panel_ids::SH4_OCRAM_CONTENTS.to_string()]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.sh4.tlb".to_string(), - label: "TLB".to_string(), - description: Some("Translation Lookaside Buffer".to_string()), - registers: None, - events: None, - actions: Some(vec![panel_ids::SH4_TLB_CONTENTS.to_string()]), - children: None, - }, - ]; - - let sh4_children = vec![ - DeviceNodeDescriptor { - path: "dc.sh4.cpu".to_string(), - label: "Core".to_string(), - description: Some("SuperH4 CPU Core".to_string()), - registers: Some(sh4_core_registers), - events: None, - actions: Some(vec![ - panel_ids::SH4_DISASSEMBLY.to_string(), - panel_ids::SH4_MEMORY.to_string(), - ]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.sh4.pbus".to_string(), - label: "PBUS".to_string(), - description: Some("Peripherals Bus".to_string()), - registers: None, - events: None, - actions: None, - children: Some(sh4_pbus_children), - }, - ]; - - let holly_children = vec![ - DeviceNodeDescriptor { - path: "dc.holly.dmac".to_string(), - label: "DMA Controller".to_string(), - description: Some("peripheral".to_string()), - registers: Some(vec![ - register("dc.holly.dmac", "DMAOR", 16), - register("dc.holly.dmac", "CHCR0", 32), - ]), - events: Some(vec![ - "dc.holly.dmac.transfer_start".to_string(), - "dc.holly.dmac.transfer_end".to_string(), - ]), - actions: None, - children: None, - }, - DeviceNodeDescriptor { - path: "dc.holly.ta".to_string(), - label: "TA".to_string(), - description: Some("Tile Accelerator".to_string()), - registers: Some(vec![ - register("dc.holly.ta", "TA_LIST_BASE", 32), - register("dc.holly.ta", "TA_STATUS", 32), - ]), - events: Some(vec![ - "dc.holly.ta.list_init".to_string(), - "dc.holly.ta.list_end".to_string(), - "dc.holly.ta.opaque_complete".to_string(), - "dc.holly.ta.translucent_complete".to_string(), - ]), - actions: Some(vec![panel_ids::CLX2_TA.to_string()]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.holly.core".to_string(), - label: "CORE".to_string(), - description: Some("Depth and Shading Engine".to_string()), - registers: Some(vec![ - register("dc.holly.core", "PVR_CTRL", 32), - register("dc.holly.core", "PVR_STATUS", 32), - ]), - events: Some(vec![ - "dc.holly.core.render_start".to_string(), - "dc.holly.core.render_end".to_string(), - "dc.holly.core.vblank".to_string(), - ]), - actions: Some(vec![panel_ids::CLX2_CORE.to_string()]), - children: None, - }, - ]; - - let sgc_channels = vec![ - DeviceNodeDescriptor { - path: "dc.aica.sgc.0".to_string(), - label: "Channel 0".to_string(), - description: Some("SGC Channel 0".to_string()), - registers: Some(vec![register("dc.aica.channels", "CH0_VOL", 8)]), - events: Some(vec![ - "dc.aica.channel.0.key_on".to_string(), - "dc.aica.channel.0.key_off".to_string(), - "dc.aica.channel.0.loop".to_string(), - ]), - actions: None, - children: None, - }, - DeviceNodeDescriptor { - path: "dc.aica.sgc.1".to_string(), - label: "Channel 1".to_string(), - description: Some("SGC Channel 1".to_string()), - registers: Some(vec![register("dc.aica.channels", "CH1_VOL", 8)]), - events: Some(vec![ - "dc.aica.channel.0.key_on".to_string(), - "dc.aica.channel.0.key_off".to_string(), - "dc.aica.channel.0.loop".to_string(), - ]), - actions: None, - children: None, - }, - ]; - - let aica_children = vec![ - DeviceNodeDescriptor { - path: "dc.aica.arm7".to_string(), - label: "ARM7".to_string(), - description: Some("ARM7DI CPU Core".to_string()), - registers: Some(vec![register("dc.aica.arm7", "PC", 32)]), - events: None, - actions: Some(vec![ - panel_ids::ARM7_DISASSEMBLY.to_string(), - panel_ids::ARM7_MEMORY.to_string(), - ]), - children: None, - }, - DeviceNodeDescriptor { - path: "dc.aica.sgc".to_string(), - label: "SGC".to_string(), - description: Some("Sound Generation Core".to_string()), - registers: None, - events: Some(vec![ - "dc.aica.channels.key_on".to_string(), - "dc.aica.channels.key_off".to_string(), - "dc.aica.channels.loop".to_string(), - ]), - actions: Some(vec![panel_ids::SGC.to_string()]), - children: Some(sgc_channels), - }, - DeviceNodeDescriptor { - path: "dc.aica.dsp".to_string(), - label: "DSP".to_string(), - description: Some("DSP VLIW Core".to_string()), - registers: Some(vec![ - register("dc.aica.dsp", "STEP", 16), - register("dc.aica.dsp", "DSP_ACC", 16), - ]), - events: Some(vec![ - "dc.aica.dsp.step".to_string(), - "dc.aica.dsp.sample_start".to_string(), - ]), - actions: Some(vec![panel_ids::DSP_DISASSEMBLY.to_string()]), - children: None, - }, - ]; - - vec![DeviceNodeDescriptor { - path: "dc".to_string(), - label: "Dreamcast".to_string(), - description: Some("beloved console".to_string()), - registers: Some(vec![ - register("dc", "SYSCLK", 0), - register("dc", "ASIC_REV", 16), - ]), - events: None, - actions: None, - children: Some(vec![ - DeviceNodeDescriptor { - path: "dc.sh4".to_string(), - label: "SH4".to_string(), - description: Some("SH7750-alike SoC".to_string()), - registers: Some(vec![ - register("dc.sh4", "VBR", 32), - register("dc.sh4", "SR", 32), - register("dc.sh4", "FPSCR", 32), - ]), - events: Some(vec![ - "dc.sh4.interrupt".to_string(), - "dc.sh4.exception".to_string(), - "dc.sh4.tlb_miss".to_string(), - ]), - actions: None, - children: Some(sh4_children), - }, - DeviceNodeDescriptor { - path: "dc.holly".to_string(), - label: "Holly".to_string(), - description: Some("System ASIC".to_string()), - registers: Some(vec![ - register("dc.holly", "HOLLY_ID", 32), - register("dc.holly", "DMAC_CTRL", 32), - ]), - events: None, - actions: None, - children: Some(holly_children), - }, - DeviceNodeDescriptor { - path: "dc.aica".to_string(), - label: "AICA".to_string(), - description: Some("Sound SoC".to_string()), - registers: Some(vec![ - register("dc.aica", "AICA_CTRL", 32), - register("dc.aica", "AICA_STATUS", 32), - ]), - events: Some(vec![ - "dc.aica.interrupt".to_string(), - "dc.aica.timer".to_string(), - ]), - actions: None, - children: Some(aica_children), - }, - ]), - }] - } - - fn set_running(&self, running: bool) { - let mut guard = self.is_running.lock().unwrap(); - *guard = running; - } - - fn increment_program_counter(&self, target: &str) { - let target_lower = target.to_ascii_lowercase(); - if target_lower.contains("sh4") { - if let Some(stripped) = self - .get_register_value("dc.sh4.cpu", "PC") - .strip_prefix("0x") - { - if let Ok(pc) = u32::from_str_radix(stripped, 16) { - let base = 0x8C0000A0; - let offset = pc.wrapping_sub(base); - let new_pc = base + ((offset + 2) % (8 * 2)); - self.set_register_value("dc.sh4.cpu", "PC", format!("0x{:08X}", new_pc)); - } - } - } else if target_lower.contains("arm7") { - if let Some(stripped) = self - .get_register_value("dc.aica.arm7", "PC") - .strip_prefix("0x") - { - if let Ok(pc) = u32::from_str_radix(stripped, 16) { - let base = 0x0020_0010; - let offset = pc.wrapping_sub(base); - let new_pc = base + ((offset + 4) % (8 * 4)); - self.set_register_value("dc.aica.arm7", "PC", format!("0x{:08X}", new_pc)); - } - } - } else if target_lower.contains("dsp") { - if let Some(stripped) = self - .get_register_value("dc.aica.dsp", "STEP") - .strip_prefix("0x") - { - if let Ok(step) = u32::from_str_radix(stripped, 16) { - let new_step = (step + 1) % 8; - self.set_register_value("dc.aica.dsp", "STEP", format!("0x{:03X}", new_step)); - } - } - } - } - - #[allow(dead_code)] - fn next_event_id(&self) -> u64 { - let mut guard = self.next_event_id.lock().unwrap(); - let id = *guard; - *guard += 1; - id - } - - #[allow(dead_code)] - fn push_event(&self, mut entry: EventLogEntry) { - if entry.event_id.is_empty() { - entry.event_id = self.next_event_id().to_string(); - } - entry.timestamp = current_timestamp_ms(); - let mut event_log = self.event_log.lock().unwrap(); - event_log.push(entry); - if event_log.len() > EVENT_LOG_LIMIT { - let remove = event_log.len() - EVENT_LOG_LIMIT; - event_log.drain(0..remove); - } - } - - pub fn build_tick(&self, dreamcast_ptr: usize, hit_breakpoint_id: Option) -> DebuggerTick { - let device_tree = self.build_device_tree(); - let all_registers = collect_registers_from_tree(&device_tree); - - let mut registers_by_id: HashMap> = HashMap::new(); - for (path, registers) in all_registers { - registers_by_id.insert(path, registers); - } - - if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut nulldc::dreamcast::Dreamcast; - - let sh4_registers = [ - ("PC", 32), - ("PR", 32), - ("SR", 32), - ("GBR", 32), - ("VBR", 32), - ("MACH", 32), - ("MACL", 32), - ("FPSCR", 32), - ("FPUL", 32), - ]; - let mut sh4_cpu_regs = Vec::new(); - for (name, width) in sh4_registers { - if let Some(value) = nulldc::dreamcast::get_sh4_register(dreamcast, name) { - sh4_cpu_regs.push(RegisterValue { - name: name.to_string(), - value: format!("0x{:08X}", value), - width, - flags: None, - metadata: None, - }); - } - } - for idx in 0..16 { - let reg_name = format!("R{}", idx); - if let Some(value) = nulldc::dreamcast::get_sh4_register(dreamcast, ®_name) { - sh4_cpu_regs.push(RegisterValue { - name: reg_name, - value: format!("0x{:08X}", value), - width: 32, - flags: None, - metadata: None, - }); - } - } - if !sh4_cpu_regs.is_empty() { - registers_by_id.insert("dc.sh4.cpu".to_string(), sh4_cpu_regs); - } - - let mut arm_regs = Vec::new(); - if let Some(value) = nulldc::dreamcast::get_arm_register(dreamcast, "PC") { - arm_regs.push(RegisterValue { - name: "PC".to_string(), - value: format!("0x{:08X}", value), - width: 32, - flags: None, - metadata: None, - }); - } - for idx in 0..16 { - let reg_name = format!("R{}", idx); - if let Some(value) = nulldc::dreamcast::get_arm_register(dreamcast, ®_name) { - arm_regs.push(RegisterValue { - name: reg_name, - value: format!("0x{:08X}", value), - width: 32, - flags: None, - metadata: None, - }); - } - } - if !arm_regs.is_empty() { - registers_by_id.insert("dc.aica.arm7".to_string(), arm_regs); - } - } - - let breakpoints_by_id = self - .breakpoints - .lock() - .unwrap() - .iter() - .map(|(id, bp)| (id.to_string(), bp.clone())) - .collect::>(); - - let watches = { - let watch_map = self.watches.lock().unwrap(); - if watch_map.is_empty() { - None - } else { - Some( - watch_map - .values() - .map(|watch| WatchDescriptor { - id: watch.id, - expression: watch.expression.clone(), - value: self.evaluate_watch_expression(dreamcast_ptr, &watch.expression), - }) - .collect::>(), - ) - } - }; - - let mut callstacks = HashMap::new(); - - let sh4_pc = if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut nulldc::dreamcast::Dreamcast; - nulldc::dreamcast::get_sh4_register(dreamcast, "PC") - .map(|value| value as u64) - .unwrap_or(0x8C00_00A0) - } else { - self.get_register_value("dc.sh4.cpu", "PC") - .strip_prefix("0x") - .and_then(|value| u64::from_str_radix(value, 16).ok()) - .unwrap_or(0x8C00_00A0) - }; - let sh4_frames = (0..16) - .map(|index| CallstackFrame { - index, - pc: if index == 0 { - sh4_pc - } else { - 0x8C00_0000 + (index - 1) as u64 * 4 - }, - sp: Some(0x0CFE_0000 - index as u64 * 16), - symbol: Some(format!("SH4_func_{}", index)), - location: Some(format!("sh4.c:{}", 100 + index)), - }) - .collect::>(); - callstacks.insert("sh4".to_string(), sh4_frames); - - let arm7_pc = if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut nulldc::dreamcast::Dreamcast; - nulldc::dreamcast::get_arm_register(dreamcast, "PC") - .map(|value| value as u64) - .unwrap_or(0x0020_0010) - } else { - self.get_register_value("dc.aica.arm7", "PC") - .strip_prefix("0x") - .and_then(|value| u64::from_str_radix(value, 16).ok()) - .unwrap_or(0x0020_0010) - }; - let arm7_frames = (0..16) - .map(|index| CallstackFrame { - index, - pc: if index == 0 { - arm7_pc - } else { - 0x0020_0000 + (index - 1) as u64 * 4 - }, - sp: Some(0x0028_0000 - index as u64 * 16), - symbol: Some(format!("ARM7_func_{}", index)), - location: Some(format!("arm7.c:{}", 100 + index)), - }) - .collect::>(); - callstacks.insert("arm7".to_string(), arm7_frames); - - let tick_id = { - let mut guard = self.tick_id.lock().unwrap(); - let id = *guard; - *guard += 1; - id - }; - - let timestamp = current_timestamp_ms(); - - let is_running = if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut nulldc::dreamcast::Dreamcast; - nulldc::dreamcast::is_dreamcast_running(dreamcast) - } else { - *self.is_running.lock().unwrap() - }; - - DebuggerTick { - tick_id, - timestamp, - execution_state: ExecutionState { - state: if is_running { - "running".to_string() - } else { - "paused".to_string() - }, - breakpoint_id: hit_breakpoint_id, - }, - registers: registers_by_id, - breakpoints: breakpoints_by_id, - event_log: self.event_log.lock().unwrap().clone(), - watches, - callstacks: Some(callstacks), - } - } -} - -fn sha256_byte(input: &str) -> u8 { - let mut hasher = Sha256::new(); - hasher.update(input.as_bytes()); - let result = hasher.finalize(); - result[0] -} - -fn generate_disassembly(target: &str, address: u64, count: usize) -> Vec { - type OperandFn = fn(u8, u8, u8, u8, u16) -> String; - - let sh4_instructions: Vec<(&str, OperandFn, u64)> = vec![ - ("mov.l", |r1, r2, _, _, _| format!("@r{}+, r{}", r1, r2), 2), - ("mov", |r1, r2, _, _, _| format!("r{}, r{}", r1, r2), 2), - ("sts.l", |r1, _, _, _, _| format!("pr, @-r{}", r1), 2), - ("add", |r1, r2, _, _, _| format!("r{}, r{}", r1, r2), 2), - ("cmp/eq", |r1, r2, _, _, _| format!("r{}, r{}", r1, r2), 2), - ("bf", |_, _, _, _, offset| format!("0x{:x}", offset), 2), - ("jmp", |r, _, _, _, _| format!("@r{}", r), 2), - ("nop", |_, _, _, _, _| String::new(), 2), - ]; - - let arm7_instructions: Vec<(&str, OperandFn, u64)> = vec![ - ("mov", |r1, _, _, val, _| format!("r{}, #{}", r1, val), 4), - ( - "ldr", - |r1, r2, _, _, offset| format!("r{}, [r{}, #{}]", r1, r2, offset), - 4, - ), - ("str", |r1, r2, _, _, _| format!("r{}, [r{}]", r1, r2), 4), - ( - "add", - |r1, r2, r3, _, _| format!("r{}, r{}, r{}", r1, r2, r3), - 4, - ), - ( - "sub", - |r1, r2, r3, _, _| format!("r{}, r{}, r{}", r1, r2, r3), - 4, - ), - ("bx", |r, _, _, _, _| format!("r{}", r), 4), - ("bl", |_, _, _, _, offset| format!("0x{:x}", offset), 4), - ("nop", |_, _, _, _, _| String::new(), 4), - ]; - - let selected = if target == "arm7" { - &arm7_instructions - } else { - &sh4_instructions - }; - - let mut lines = Vec::new(); - let mut current_addr = address; - - for _ in 0..count { - let hash = sha256_byte(&format!("{}:{:x}", target, current_addr)); - let instr_index = (hash as usize) % selected.len(); - let (mnemonic, operand_fn, bytes_len) = selected[instr_index]; - - let r1 = (hash >> 4) % 16; - let r2 = (hash >> 2) % 16; - let r3 = hash % 16; - let val = (hash.wrapping_mul(3)) & 0xff; - let offset = (hash.wrapping_mul(7) as u16) & 0xfff; - - let operands = operand_fn(r1, r2, r3, val, offset); - let disassembly = if operands.is_empty() { - mnemonic.to_string() - } else { - format!("{} {}", mnemonic, operands) - }; - - let byte_values: Vec = (0..bytes_len) - .map(|b| { - format!( - "{:02X}", - sha256_byte(&format!("{}:{:x}:{}", target, current_addr, b)) - ) - }) - .collect(); - - lines.push(DisassemblyLine { - address: current_addr, - bytes: byte_values.join(" "), - disassembly, - }); - - current_addr += bytes_len; - } - - lines -} - -fn build_memory_slice( - dreamcast_ptr: usize, - target: &str, - address: Option, - length: Option, -) -> MemorySlice { - let default_base = match target { - "sh4" => 0x8c000000u64, - "arm7" => 0x00200000u64, - "dsp" => 0x00000000u64, - _ => 0x8c000000u64, - }; - - let base_address = address.unwrap_or(default_base); - let effective_length = length.unwrap_or(64); - - // Try to read from actual emulator memory if pointer is valid - let bytes: Vec = if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut nulldc::dreamcast::Dreamcast; - match target { - "arm7" => { - nulldc::dreamcast::read_arm_memory_slice(dreamcast, base_address, effective_length) - } - _ => nulldc::dreamcast::read_memory_slice(dreamcast, base_address, effective_length), - } - } else { - // Fall back to mock data if no emulator context - (0..effective_length) - .map(|i| sha256_byte(&format!("{}:{:x}", target, base_address + i as u64))) - .collect() - }; - - MemorySlice { - base_address, - data: bytes, - validity: "ok".to_string(), - } -} - -fn collect_registers_from_tree(tree: &[DeviceNodeDescriptor]) -> Vec<(String, Vec)> { - let mut result = Vec::new(); - for node in tree { - if let Some(ref registers) = node.registers { - if !registers.is_empty() { - result.push((node.path.clone(), registers.clone())); - } - } - if let Some(ref children) = node.children { - result.extend(collect_registers_from_tree(children)); - } - } - result -} - -pub fn handle_request( - state: Arc, - dreamcast_ptr: usize, - request: JsonRpcRequest, -) -> Result<(serde_json::Value, bool), JsonRpcErrorObject> { - let params = request.params.unwrap_or(json!({})); - - match request.method.as_str() { - "debugger.describe" => { - let device_tree = state.build_device_tree(); - Ok(( - json!({ - "emulator": { - "name": "mockServer", - "version": "unspecified", - "build": "native" - }, - "deviceTree": device_tree, - "capabilities": CAPABILITIES, - }), - true, - )) - } - - "state.getMemorySlice" => { - let target = params - .get("target") - .and_then(|value| value.as_str()) - .unwrap_or("sh4"); - let address = params.get("address").and_then(|value| value.as_u64()); - let length = params - .get("length") - .and_then(|value| value.as_u64()) - .map(|value| value as usize); - let slice = build_memory_slice(dreamcast_ptr, target, address, length); - Ok((serde_json::to_value(slice).unwrap(), false)) - } - - "state.getDisassembly" => { - let target = params - .get("target") - .and_then(|value| value.as_str()) - .unwrap_or("sh4"); - let address = params - .get("address") - .and_then(|value| value.as_u64()) - .unwrap_or(0); - let count = params - .get("count") - .and_then(|value| value.as_u64()) - .unwrap_or(128) as usize; - - let lines = if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut nulldc::dreamcast::Dreamcast; - match target { - "sh4" => nulldc::dreamcast::disassemble_sh4(dreamcast, address, count) - .into_iter() - .map(|line| DisassemblyLine { - address: line.address, - bytes: line.bytes, - disassembly: line.disassembly, - }) - .collect::>(), - "arm7" => nulldc::dreamcast::disassemble_arm7(dreamcast, address, count) - .into_iter() - .map(|line| DisassemblyLine { - address: line.address, - bytes: line.bytes, - disassembly: line.disassembly, - }) - .collect::>(), - _ => generate_disassembly(target, address, count), - } - } else { - generate_disassembly(target, address, count) - }; - - Ok((json!({ "lines": lines }), false)) - } - - "state.getCallstack" => { - let target = params - .get("target") - .and_then(|value| value.as_str()) - .unwrap_or("sh4"); - let max_frames = params - .get("maxFrames") - .and_then(|value| value.as_u64()) - .unwrap_or(16) - .min(64) as usize; - - let frames: Vec = (0..max_frames) - .map(|index| CallstackFrame { - index: index as u32, - pc: 0x8c000000 + index as u64 * 4, - sp: Some(0x0cfe0000 - index as u64 * 16), - symbol: Some(format!("{}_func_{}", target.to_uppercase(), index)), - location: Some(format!("{}.c:{}", target, 100 + index)), - }) - .collect(); - - Ok((json!({ "target": target, "frames": frames }), false)) - } - - "state.watch" => { - let expressions = params - .get("expressions") - .and_then(|value| value.as_array()) - .cloned() - .unwrap_or_default() - .into_iter() - .filter_map(|value| value.as_str().map(|s| s.to_owned())) - .collect::>(); - - let mut watches = state.watches.lock().unwrap(); - let mut next_id = state.next_watch_id.lock().unwrap(); - - for expr in expressions { - let id = *next_id; - watches.insert( - id, - ServerWatch { - id, - expression: expr, - }, - ); - *next_id += 1; - } - - Ok((json!({}), true)) - } - - "state.unwatch" => { - let watch_ids = params - .get("watchIds") - .and_then(|value| value.as_array()) - .cloned() - .unwrap_or_default() - .into_iter() - .filter_map(|value| value.as_u64()) - .map(|value| value as u32) - .collect::>(); - - let mut watches = state.watches.lock().unwrap(); - for id in watch_ids { - watches.remove(&id); - } - - Ok((json!({}), true)) - } - - "state.editWatch" => { - let watch_id = params - .get("watchId") - .and_then(|value| value.as_u64()) - .map(|value| value as u32); - let value = params - .get("value") - .and_then(|value| value.as_str()) - .unwrap_or(""); - - if let Some(id) = watch_id { - let expression = { - let watches = state.watches.lock().unwrap(); - watches.get(&id).map(|watch| watch.expression.clone()) - }; - - if let Some(expr) = expression { - let parts: Vec<&str> = expr.split('.').collect(); - let (path, name) = if parts.len() > 1 { - let name = parts.last().unwrap(); - let path = parts[..parts.len() - 1].join("."); - (path, name.to_string()) - } else { - ("dc.sh4.cpu".to_string(), parts[0].to_string()) - }; - let key = format!("{}.{}", path, name.to_lowercase()); - - { - let registers = state.register_values.lock().unwrap(); - if !registers.contains_key(&key) { - return Ok(( - json!({ - "error": { - "code": -32602, - "message": format!( - "Cannot edit non-register expression \"{}\"", - expr - ), - } - }), - false, - )); - } - } - - state.set_register_value(&path, &name, value.to_string()); - return Ok((json!({}), true)); - } - - return Ok(( - json!({ - "error": { - "code": -32602, - "message": format!("Watch \"{}\" not found", id), - } - }), - false, - )); - } - - Ok(( - json!({ - "error": { - "code": -32602, - "message": "Watch not found or cannot edit", - } - }), - false, - )) - } - - "state.modifyWatchExpression" => { - let watch_id = params - .get("watchId") - .and_then(|value| value.as_u64()) - .map(|value| value as u32); - let new_expression = params - .get("newExpression") - .and_then(|value| value.as_str()) - .unwrap_or(""); - - if let Some(id) = watch_id { - let mut watches = state.watches.lock().unwrap(); - if let Some(watch) = watches.get_mut(&id) { - watch.expression = new_expression.to_string(); - return Ok((json!({}), true)); - } - - return Ok(( - json!({ - "error": { - "code": -32602, - "message": format!("Watch {} not found", id), - } - }), - false, - )); - } - - Ok(( - json!({ - "error": { - "code": -32602, - "message": "Watch not found", - } - }), - false, - )) - } - - "control.pause" => { - if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut nulldc::dreamcast::Dreamcast; - nulldc::dreamcast::set_dreamcast_running(dreamcast, false); - } - state.set_running(false); - Ok((json!({}), true)) - } - - "control.step" | "control.stepOver" | "control.stepOut" => { - let target = params - .get("target") - .and_then(|value| value.as_str()) - .unwrap_or("sh4"); - - if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut nulldc::dreamcast::Dreamcast; - nulldc::dreamcast::step_dreamcast(dreamcast); - nulldc::dreamcast::set_dreamcast_running(dreamcast, false); - } else { - state.increment_program_counter(target); - } - - state.set_running(false); - Ok((json!({}), true)) - } - - "control.runUntil" => { - if dreamcast_ptr != 0 { - let dreamcast = dreamcast_ptr as *mut nulldc::dreamcast::Dreamcast; - nulldc::dreamcast::set_dreamcast_running(dreamcast, true); - } - state.set_running(true); - Ok((json!({}), true)) - } - - "breakpoints.add" => { - let event = params - .get("event") - .and_then(|value| value.as_str()) - .unwrap_or(""); - let address = params.get("address").and_then(|value| value.as_u64()); - let kind = params - .get("kind") - .and_then(|value| value.as_str()) - .unwrap_or("code"); - let enabled = params - .get("enabled") - .and_then(|value| value.as_bool()) - .unwrap_or(true); - - let mut next_id = state.next_breakpoint_id.lock().unwrap(); - let id = *next_id; - *next_id += 1; - - let breakpoint = BreakpointDescriptor { - id, - event: event.to_string(), - address, - kind: kind.to_string(), - enabled, - }; - - state.breakpoints.lock().unwrap().insert(id, breakpoint); - Ok((json!({}), true)) - } - - "breakpoints.remove" => { - if let Some(id) = params - .get("id") - .and_then(|value| value.as_u64()) - .map(|value| value as u32) - { - let removed = state.breakpoints.lock().unwrap().remove(&id).is_some(); - if removed { - return Ok((json!({}), true)); - } - - return Ok(( - json!({ - "error": { - "code": -32000, - "message": format!("Breakpoint {} not found", id), - } - }), - false, - )); - } - - Ok((json!({}), false)) - } - - "breakpoints.toggle" => { - let id = params - .get("id") - .and_then(|value| value.as_u64()) - .map(|value| value as u32); - let enabled = params - .get("enabled") - .and_then(|value| value.as_bool()) - .unwrap_or(true); - - if let Some(id) = id { - let mut breakpoints = state.breakpoints.lock().unwrap(); - if let Some(bp) = breakpoints.get_mut(&id) { - bp.enabled = enabled; - return Ok((json!({}), true)); - } - - return Ok(( - json!({ - "error": { - "code": -32000, - "message": format!("Breakpoint {} not found", id), - } - }), - false, - )); - } - - Ok((json!({}), false)) - } - - "breakpoints.setCategoryStates" => { - if let Some(categories) = params.get("categories").and_then(|value| value.as_object()) { - let mut category_states = state.category_states.lock().unwrap(); - for (category, state_value) in categories { - if let (Some(muted), Some(soloed)) = ( - state_value.get("muted").and_then(|value| value.as_bool()), - state_value.get("soloed").and_then(|value| value.as_bool()), - ) { - category_states - .insert(category.clone(), BreakpointCategoryState { muted, soloed }); - } - } - } - Ok((json!({}), true)) - } - - _ => Err(JsonRpcErrorObject { - code: -32601, - message: format!("Method not found: {}", request.method), - data: None, - }), - } -} - -pub async fn handle_websocket_connection(socket: WebSocket, dreamcast_ptr: usize) { - use std::sync::OnceLock; - static STATE: OnceLock> = OnceLock::new(); - let state = STATE.get_or_init(|| Arc::new(ServerState::new())).clone(); - - // Convert usize back to *mut Dreamcast when needed to access emulator state - // let _dreamcast = dreamcast_ptr as *mut nulldc::dreamcast::Dreamcast; - // TODO: Use dreamcast pointer to read/write actual emulator state instead of mock data - - let (mut sender, mut receiver) = socket.split(); - - while let Some(Ok(msg)) = receiver.next().await { - match msg { - Message::Text(text) => { - if let Ok(request) = serde_json::from_str::(&text) { - let id = request.id.clone(); - match handle_request(state.clone(), dreamcast_ptr, request) { - Ok((result, should_broadcast)) => { - let response = JsonRpcSuccess { - jsonrpc: JSON_RPC_VERSION.to_string(), - id, - result, - }; - if let Ok(json) = serde_json::to_string(&response) { - let _ = sender.send(Message::Text(json.into())).await; - } - - if should_broadcast { - let tick = state.build_tick(dreamcast_ptr, None); - let notification = JsonRpcNotification { - jsonrpc: JSON_RPC_VERSION.to_string(), - method: "event.tick".to_string(), - params: serde_json::to_value(tick).unwrap(), - }; - - if let Ok(json) = serde_json::to_string(¬ification) { - let _ = sender.send(Message::Text(json.into())).await; - } - } - } - Err(error) => { - let response = JsonRpcError { - jsonrpc: JSON_RPC_VERSION.to_string(), - id, - error, - }; - if let Ok(json) = serde_json::to_string(&response) { - let _ = sender.send(Message::Text(json.into())).await; - } - } - } - } - } - Message::Close(_) => break, - _ => {} - } - } -} From 743b5c68abff278da309c9f656b135d5dd52f7f0 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Tue, 14 Oct 2025 22:25:16 +0300 Subject: [PATCH 13/15] gitignore: update --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 73e3393..a849207 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ target/ dist/ .vscode/* !.vscode/extensions.json -.claude/ \ No newline at end of file +.claude/ +nul \ No newline at end of file From b6c28475e26ed0d2d161e9e14c12c32827b26ea1 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Tue, 14 Oct 2025 22:28:46 +0300 Subject: [PATCH 14/15] homepage: remove link to mock debug server --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 4d7a757..04b1197 100644 --- a/index.html +++ b/index.html @@ -89,7 +89,7 @@

Latest CI/CD downloads

Go to the builds page.

WASM embed

-

Open Devtools for WASM embed | Open Devtools w/ mock server

+

Open Devtools for WASM embed

From 248dc4add00c117dde1415ccc982d19df5aa8079 Mon Sep 17 00:00:00 2001 From: Stefanos Kornilios Mitsis Poiitidis Date: Tue, 14 Oct 2025 23:19:51 +0300 Subject: [PATCH 15/15] emu: Restrict menus to windows/macOS as linux brings in gtk and can't cross compile with that in CI --- Cargo.toml | 3 ++- src/lib.rs | 29 ++++++++++++++++++----------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ef3733a..756d799 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,7 +71,8 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" uuid = { version = "1.0", features = ["v4"] } -# Menu support +[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] +# Menu support (Windows and macOS only, to avoid glib dependency on Linux) muda = "0.15" open = "5.0" diff --git a/src/lib.rs b/src/lib.rs index 9febd08..578dbc3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ use wasm_logger; #[cfg(target_arch = "wasm32")] use wgpu::web_sys; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(any(target_os = "windows", target_os = "macos"))] use muda::{Menu, MenuItem}; use winit::window::Window; @@ -451,9 +451,9 @@ impl State { struct App { state: Option, dreamcast: *mut Dreamcast, - #[cfg(not(target_arch = "wasm32"))] + #[cfg(any(target_os = "windows", target_os = "macos"))] menu: Option, - #[cfg(not(target_arch = "wasm32"))] + #[cfg(any(target_os = "windows", target_os = "macos"))] devtools_item: Option, } @@ -462,9 +462,9 @@ impl Default for App { Self { state: None, dreamcast: std::ptr::null_mut(), - #[cfg(not(target_arch = "wasm32"))] + #[cfg(any(target_os = "windows", target_os = "macos"))] menu: None, - #[cfg(not(target_arch = "wasm32"))] + #[cfg(any(target_os = "windows", target_os = "macos"))] devtools_item: None, } } @@ -483,9 +483,9 @@ impl AppHandle { AppHandle(Rc::new(RefCell::new(App { state: None, dreamcast: dreamcast, - #[cfg(not(target_arch = "wasm32"))] + #[cfg(any(target_os = "windows", target_os = "macos"))] menu: None, - #[cfg(not(target_arch = "wasm32"))] + #[cfg(any(target_os = "windows", target_os = "macos"))] devtools_item: None, }))) } @@ -533,7 +533,7 @@ impl ApplicationHandler for AppHandle { let window = Arc::new(event_loop.create_window(window_attributes).unwrap()); - #[cfg(not(target_arch = "wasm32"))] + #[cfg(any(target_os = "windows", target_os = "macos"))] { // Create menu let menu = Menu::new(); @@ -554,13 +554,20 @@ impl ApplicationHandler for AppHandle { } } - let state = pollster::block_on(State::new(window)); + let state = pollster::block_on(State::new(window.clone())); let mut app = self.0.borrow_mut(); app.state = Some(state); app.menu = Some(menu); app.devtools_item = Some(devtools_item); } + #[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows"), not(target_os = "macos")))] + { + let state = pollster::block_on(State::new(window)); + let mut app = self.0.borrow_mut(); + app.state = Some(state); + } + #[cfg(target_arch = "wasm32")] { use std::cell::RefCell; @@ -653,8 +660,8 @@ impl ApplicationHandler for AppHandle { fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { let app = self.0.borrow_mut(); - // Handle menu events (non-WASM only) - #[cfg(not(target_arch = "wasm32"))] + // Handle menu events (Windows and macOS only) + #[cfg(any(target_os = "windows", target_os = "macos"))] { if let Some(ref devtools_item) = app.devtools_item { if let Ok(event) = muda::MenuEvent::receiver().try_recv() {