From 37f53616ff0330d8b80116f4274fdbafb1d8f600 Mon Sep 17 00:00:00 2001 From: Simen Svale Date: Tue, 30 Dec 2025 01:56:03 +0100 Subject: [PATCH] Add semantic layer tools for interpreted C64 debugging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New tools: - readScreen: Screen RAM as ASCII text with non-empty line summary - readColorRam: Color RAM with color names and usage statistics - readVicState: Full VIC-II state with interpreted values - Graphics mode detection - Memory bank/address calculation - Sprite enable summary - readSprites: All 8 sprites with position, visibility, colors - Visibility checks with explanations - Data pointer address resolution - enabledOnly filtering option Utilities (src/utils/c64.ts): - PETSCII screen code to ASCII conversion - C64 color palette names - VIC bank and memory address calculation - Graphics mode detection - Sprite visibility range checks AX patterns applied: - Summary modes (readColorRam summary option) - Filtering (readSprites enabledOnly option) - Rich hints explaining issues (sprite visibility) - Proactive issue detection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/index.ts | 427 +++++++++++++++++++++++++++++++++++++++++++++ src/utils/c64.ts | 180 +++++++++++++++++++ src/utils/index.ts | 1 + 3 files changed, 608 insertions(+) create mode 100644 src/utils/c64.ts create mode 100644 src/utils/index.ts diff --git a/src/index.ts b/src/index.ts index 93504ba..0bdd57f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,14 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { getViceClient, ViceError } from "./protocol/index.js"; +import { + getColorInfo, + screenToText, + getVicBank, + getVideoAddresses, + getGraphicsMode, + isSpriteVisible, +} from "./utils/index.js"; const server = new McpServer({ name: "vice-mcp", @@ -647,6 +655,425 @@ Related tools: setBreakpoint, deleteBreakpoint`, } ); +// ============================================================================= +// SEMANTIC LAYER TOOLS - Interpreted output for autonomous debugging +// ============================================================================= + +// Tool: readScreen - Get screen contents as text +server.registerTool( + "readScreen", + { + description: `Read the C64 screen memory and return it as interpreted text. + +Converts PETSCII screen codes to readable ASCII. Returns 25 lines of 40 characters. + +Use this instead of readMemory($0400) when you want to see what's displayed on screen. + +Note: This reads from the current screen RAM location (may not be $0400 if the program moved it). +In bitmap modes, the data won't represent text. + +Options: +- includeRaw: Also return raw screen codes (default: false) + +Related tools: readColorRam, readVicState, readMemory`, + inputSchema: z.object({ + includeRaw: z + .boolean() + .optional() + .describe("Include raw screen codes array (default: false)"), + }), + }, + async (args) => { + try { + // First get VIC bank from CIA2 + const cia2Data = await client.readMemory(0xdd00, 0xdd00); + const bankInfo = getVicBank(cia2Data[0]); + + // Get screen address from $D018 + const d018Data = await client.readMemory(0xd018, 0xd018); + const videoAddrs = getVideoAddresses(d018Data[0], bankInfo.baseAddress); + + // Read screen RAM (1000 bytes) + const screenData = await client.readMemory( + videoAddrs.screenAddress, + videoAddrs.screenAddress + 999 + ); + + // Convert to text + const textLines = screenToText(screenData); + + // Find non-empty lines for summary + const nonEmptyLines = textLines + .map((line, idx) => ({ line: idx, content: line })) + .filter((l) => l.content.trim().length > 0); + + const response: Record = { + screenAddress: { + value: videoAddrs.screenAddress, + hex: `$${videoAddrs.screenAddress.toString(16).padStart(4, "0")}`, + }, + vicBank: bankInfo.bank, + lines: textLines, + summary: { + nonEmptyLines: nonEmptyLines.length, + preview: + nonEmptyLines.length > 0 + ? nonEmptyLines.slice(0, 3).map((l) => `Line ${l.line}: "${l.content}"`) + : ["Screen appears empty"], + }, + }; + + if (args.includeRaw) { + response.raw = Array.from(screenData); + } + + // Check graphics mode and add hint + const d011Data = await client.readMemory(0xd011, 0xd011); + const d016Data = await client.readMemory(0xd016, 0xd016); + const graphicsMode = getGraphicsMode(d011Data[0], d016Data[0]); + + response.graphicsMode = graphicsMode.mode; + + if (graphicsMode.bitmap) { + response.hint = + "Warning: VIC-II is in bitmap mode - screen RAM contains bitmap data, not text."; + } else if (nonEmptyLines.length === 0) { + response.hint = "Screen appears empty or contains only spaces."; + } else { + response.hint = `Screen has ${nonEmptyLines.length} non-empty line(s). First: "${nonEmptyLines[0]?.content || ""}"`; + } + + return formatResponse(response); + } catch (error) { + return formatError(error as ViceError); + } + } +); + +// Tool: readColorRam - Get color RAM state +server.registerTool( + "readColorRam", + { + description: `Read color RAM ($D800-$DBE7) and return color values with names. + +Color RAM determines the foreground color of each character on screen. + +Returns: +- 25x40 grid of color values (0-15) with names +- Summary of colors used + +Related tools: readScreen, readVicState`, + inputSchema: z.object({ + summary: z + .boolean() + .optional() + .describe("Return only color usage summary, not full grid (default: false)"), + }), + }, + async (args) => { + try { + // Color RAM is always at $D800 + const colorData = await client.readMemory(0xd800, 0xd800 + 999); + + // Count color usage + const colorCounts = new Map(); + for (const byte of colorData) { + const color = byte & 0x0f; + colorCounts.set(color, (colorCounts.get(color) || 0) + 1); + } + + // Sort by frequency + const colorUsage = Array.from(colorCounts.entries()) + .sort((a, b) => b[1] - a[1]) + .map(([color, count]) => ({ + color: getColorInfo(color), + count, + percentage: Math.round((count / 1000) * 100), + })); + + const response: Record = { + address: { value: 0xd800, hex: "$D800" }, + summary: { + uniqueColors: colorUsage.length, + dominantColor: colorUsage[0]?.color || null, + usage: colorUsage, + }, + }; + + if (!args.summary) { + // Convert to 25 lines of 40 color values + const colorLines: Array> = []; + for (let row = 0; row < 25; row++) { + const line: Array<{ value: number; name: string }> = []; + for (let col = 0; col < 40; col++) { + const offset = row * 40 + col; + line.push(getColorInfo(colorData[offset])); + } + colorLines.push(line); + } + response.grid = colorLines; + } + + response.hint = + colorUsage.length === 1 + ? `Entire screen uses ${colorUsage[0].color.name} (${colorUsage[0].color.value})` + : `${colorUsage.length} colors used. Dominant: ${colorUsage[0]?.color.name} (${colorUsage[0]?.percentage}%)`; + + return formatResponse(response); + } catch (error) { + return formatError(error as ViceError); + } + } +); + +// Tool: readVicState - Full VIC-II chip state +server.registerTool( + "readVicState", + { + description: `Read the full VIC-II state with interpreted values. + +Returns all VIC-II registers with semantic meaning: +- Border and background colors (with names) +- Graphics mode (text, bitmap, multicolor, etc.) +- Screen and character memory locations +- Scroll values +- Raster position +- Sprite enable bits + +This is the high-level view of the video chip. Use for understanding display configuration. + +Related tools: readScreen, readSprites, readMemory (for $D000-$D02E)`, + }, + async () => { + try { + // Read all VIC registers $D000-$D02E (47 bytes) + const vicData = await client.readMemory(0xd000, 0xd02e); + + // Read CIA2 for bank info + const cia2Data = await client.readMemory(0xdd00, 0xdd00); + const bankInfo = getVicBank(cia2Data[0]); + + const d011 = vicData[0x11]; + const d016 = vicData[0x16]; + const d018 = vicData[0x18]; + + const graphicsMode = getGraphicsMode(d011, d016); + const videoAddrs = getVideoAddresses(d018, bankInfo.baseAddress); + + // Raster position (9-bit) + const rasterLine = vicData[0x12] | ((d011 & 0x80) << 1); + + // Sprite enable + const spriteEnable = vicData[0x15]; + const enabledSprites = []; + for (let i = 0; i < 8; i++) { + if (spriteEnable & (1 << i)) enabledSprites.push(i); + } + + // Display enable + const displayEnabled = !!(d011 & 0x10); + + const response = { + // Colors + borderColor: getColorInfo(vicData[0x20]), + backgroundColor: [ + getColorInfo(vicData[0x21]), + getColorInfo(vicData[0x22]), + getColorInfo(vicData[0x23]), + getColorInfo(vicData[0x24]), + ], + + // Graphics mode + graphicsMode: graphicsMode.mode, + displayEnabled, + bitmap: graphicsMode.bitmap, + multicolor: graphicsMode.multicolor, + extendedColor: graphicsMode.extendedColor, + + // Screen geometry + rows: d011 & 0x08 ? 25 : 24, + columns: d016 & 0x08 ? 40 : 38, + scrollX: d016 & 0x07, + scrollY: d011 & 0x07, + + // Memory setup + vicBank: { + bank: bankInfo.bank, + baseAddress: { + value: bankInfo.baseAddress, + hex: `$${bankInfo.baseAddress.toString(16).padStart(4, "0")}`, + }, + }, + screenAddress: { + value: videoAddrs.screenAddress, + hex: `$${videoAddrs.screenAddress.toString(16).padStart(4, "0")}`, + }, + charAddress: { + value: videoAddrs.charAddress, + hex: `$${videoAddrs.charAddress.toString(16).padStart(4, "0")}`, + }, + + // Raster + rasterLine, + + // Sprites summary + spriteEnable: { + value: spriteEnable, + binary: spriteEnable.toString(2).padStart(8, "0"), + enabledSprites, + count: enabledSprites.length, + }, + + // Sprite multicolor registers + spriteMulticolor0: getColorInfo(vicData[0x25]), + spriteMulticolor1: getColorInfo(vicData[0x26]), + + hint: !displayEnabled + ? "Display is blanked (DEN=0) - screen shows border color only" + : enabledSprites.length > 0 + ? `${graphicsMode.mode} mode, ${enabledSprites.length} sprite(s) enabled. Use readSprites() for sprite details.` + : `${graphicsMode.mode} mode, no sprites enabled.`, + }; + + return formatResponse(response); + } catch (error) { + return formatError(error as ViceError); + } + } +); + +// Tool: readSprites - Get detailed sprite state +server.registerTool( + "readSprites", + { + description: `Read state of all 8 hardware sprites with interpreted values. + +Returns for each sprite: +- Position (X, Y) with visibility check +- Color (with name) +- Enable status +- Multicolor mode +- X/Y expansion (double size) +- Priority (in front of / behind background) +- Data pointer address + +Use this to debug sprite issues like: +- "Why is my sprite invisible?" → check enabled, position, pointer +- "Wrong colors?" → check multicolor mode and color registers +- "Wrong size?" → check expand flags + +Options: +- enabledOnly: Only return enabled sprites (default: false) + +Related tools: readVicState, readMemory (for sprite data)`, + inputSchema: z.object({ + enabledOnly: z + .boolean() + .optional() + .describe("Only return enabled sprites (default: false)"), + }), + }, + async (args) => { + try { + // Read VIC registers + const vicData = await client.readMemory(0xd000, 0xd02e); + + // Read CIA2 for bank info (needed for sprite pointer calculation) + const cia2Data = await client.readMemory(0xdd00, 0xdd00); + const bankInfo = getVicBank(cia2Data[0]); + + // Get screen address for sprite pointers + const d018 = vicData[0x18]; + const videoAddrs = getVideoAddresses(d018, bankInfo.baseAddress); + + // Sprite pointers are at screen + $3F8 + const spritePointerBase = videoAddrs.screenAddress + 0x3f8; + const spritePointers = await client.readMemory( + spritePointerBase, + spritePointerBase + 7 + ); + + const spriteEnable = vicData[0x15]; + const spriteXMsb = vicData[0x10]; + const spriteYExpand = vicData[0x17]; + const spriteXExpand = vicData[0x1d]; + const spriteMulticolor = vicData[0x1c]; + const spritePriority = vicData[0x1b]; + + const sprites = []; + + for (let i = 0; i < 8; i++) { + const enabled = !!(spriteEnable & (1 << i)); + + // Skip disabled sprites if enabledOnly + if (args.enabledOnly && !enabled) continue; + + // X position (9-bit) + const xLow = vicData[i * 2]; + const xHigh = (spriteXMsb & (1 << i)) ? 256 : 0; + const x = xLow + xHigh; + + // Y position (8-bit) + const y = vicData[i * 2 + 1]; + + const visibility = isSpriteVisible(x, y, enabled); + + // Sprite data address + const pointer = spritePointers[i]; + const dataAddress = bankInfo.baseAddress + pointer * 64; + + sprites.push({ + index: i, + enabled, + position: { + x, + y, + visible: visibility.visible, + visibilityReason: visibility.reason, + }, + color: getColorInfo(vicData[0x27 + i]), + multicolor: !!(spriteMulticolor & (1 << i)), + expandX: !!(spriteXExpand & (1 << i)), + expandY: !!(spriteYExpand & (1 << i)), + priority: (spritePriority & (1 << i)) ? "behind" : "front", + pointer: { + value: pointer, + hex: `$${pointer.toString(16).padStart(2, "0")}`, + }, + dataAddress: { + value: dataAddress, + hex: `$${dataAddress.toString(16).padStart(4, "0")}`, + }, + }); + } + + const enabledCount = sprites.filter((s) => s.enabled).length; + const visibleCount = sprites.filter((s) => s.position.visible).length; + const issues = sprites + .filter((s) => s.enabled && !s.position.visible) + .map((s) => `Sprite ${s.index}: ${s.position.visibilityReason}`); + + return formatResponse({ + count: sprites.length, + enabledCount, + visibleCount, + sprites, + spriteMulticolor0: getColorInfo(vicData[0x25]), + spriteMulticolor1: getColorInfo(vicData[0x26]), + issues: issues.length > 0 ? issues : undefined, + hint: + issues.length > 0 + ? `${issues.length} enabled sprite(s) not visible: ${issues[0]}` + : enabledCount === 0 + ? "No sprites enabled" + : `${enabledCount} sprite(s) enabled, ${visibleCount} visible`, + }); + } catch (error) { + return formatError(error as ViceError); + } + } +); + async function main() { const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/src/utils/c64.ts b/src/utils/c64.ts new file mode 100644 index 0000000..4824a45 --- /dev/null +++ b/src/utils/c64.ts @@ -0,0 +1,180 @@ +// C64 Constants and Utilities + +// Color palette names +export const C64_COLORS = [ + "black", + "white", + "red", + "cyan", + "purple", + "green", + "blue", + "yellow", + "orange", + "brown", + "light red", + "dark gray", + "gray", + "light green", + "light blue", + "light gray", +] as const; + +export type C64Color = (typeof C64_COLORS)[number]; + +export function getColorName(value: number): C64Color { + return C64_COLORS[value & 0x0f]; +} + +export function getColorInfo(value: number): { value: number; name: C64Color } { + return { + value: value & 0x0f, + name: getColorName(value), + }; +} + +// PETSCII to ASCII conversion (screen codes, not PETSCII character codes) +// Screen codes are different from PETSCII - this handles the screen memory values +export function screenCodeToAscii(code: number): string { + // Screen codes 0-31: @, A-Z, [, £, ], ↑, ← + if (code <= 31) { + if (code === 0) return "@"; + if (code <= 26) return String.fromCharCode(64 + code); // A-Z + if (code === 27) return "["; + if (code === 28) return "£"; + if (code === 29) return "]"; + if (code === 30) return "^"; // up arrow + if (code === 31) return "<"; // left arrow + } + + // 32-63: space, !, ", #, $, %, &, ', (, ), *, +, ,, -, ., /, 0-9, :, ;, <, =, >, ? + if (code >= 32 && code <= 63) { + return String.fromCharCode(code); + } + + // 64-95: horizontal line, graphics chars - use placeholders + if (code >= 64 && code <= 95) { + return "#"; // graphics placeholder + } + + // 96-127: more graphics + if (code >= 96 && code <= 127) { + return "#"; // graphics placeholder + } + + // 128-255: reverse video versions of 0-127 + if (code >= 128) { + // Just show the base character (non-reversed) + return screenCodeToAscii(code - 128); + } + + return "?"; +} + +// Convert screen RAM to text lines +export function screenToText(screenData: Buffer | number[]): string[] { + const data = Buffer.isBuffer(screenData) ? screenData : Buffer.from(screenData); + const lines: string[] = []; + + for (let row = 0; row < 25; row++) { + const offset = row * 40; + let line = ""; + for (let col = 0; col < 40; col++) { + if (offset + col < data.length) { + line += screenCodeToAscii(data[offset + col]); + } + } + // Trim trailing spaces but keep the line + lines.push(line.trimEnd()); + } + + return lines; +} + +// VIC-II memory bank calculation +export function getVicBank(cia2PortA: number): { bank: number; baseAddress: number } { + // CIA2 port A bits 0-1 (inverted) select the bank + const bankBits = (~cia2PortA) & 0x03; + return { + bank: bankBits, + baseAddress: bankBits * 0x4000, + }; +} + +// Screen and character base from $D018 +export function getVideoAddresses( + d018: number, + bankBase: number +): { screenAddress: number; charAddress: number } { + const screenOffset = ((d018 >> 4) & 0x0f) * 0x0400; + const charOffset = ((d018 >> 1) & 0x07) * 0x0800; + + return { + screenAddress: bankBase + screenOffset, + charAddress: bankBase + charOffset, + }; +} + +// Graphics mode from D011 and D016 +export function getGraphicsMode(d011: number, d016: number): { + mode: string; + bitmap: boolean; + multicolor: boolean; + extendedColor: boolean; +} { + const ecm = !!(d011 & 0x40); + const bmm = !!(d011 & 0x20); + const mcm = !!(d016 & 0x10); + + let mode = "standard text"; + if (ecm && !bmm && !mcm) mode = "extended background color"; + else if (!ecm && !bmm && mcm) mode = "multicolor text"; + else if (!ecm && bmm && !mcm) mode = "standard bitmap"; + else if (!ecm && bmm && mcm) mode = "multicolor bitmap"; + else if (ecm) mode = "invalid (ECM + other modes)"; + + return { + mode, + bitmap: bmm, + multicolor: mcm, + extendedColor: ecm, + }; +} + +// Sprite position visible range helpers +export const SPRITE_VISIBLE_X_MIN = 24; +export const SPRITE_VISIBLE_X_MAX = 343; +export const SPRITE_VISIBLE_Y_MIN = 50; +export const SPRITE_VISIBLE_Y_MAX = 249; + +export function isSpriteVisible(x: number, y: number, enabled: boolean): { + visible: boolean; + reason?: string; +} { + if (!enabled) { + return { visible: false, reason: "Sprite is disabled ($D015)" }; + } + if (x < SPRITE_VISIBLE_X_MIN || x > SPRITE_VISIBLE_X_MAX) { + return { visible: false, reason: `X position ${x} is outside visible range (${SPRITE_VISIBLE_X_MIN}-${SPRITE_VISIBLE_X_MAX})` }; + } + if (y < SPRITE_VISIBLE_Y_MIN || y > SPRITE_VISIBLE_Y_MAX) { + return { visible: false, reason: `Y position ${y} is outside visible range (${SPRITE_VISIBLE_Y_MIN}-${SPRITE_VISIBLE_Y_MAX})` }; + } + return { visible: true }; +} + +// Memory region descriptions +export function describeAddress(address: number): string { + if (address < 0x0100) return "Zero page"; + if (address < 0x0200) return "Stack"; + if (address >= 0x0400 && address < 0x0800) return "Default screen RAM"; + if (address >= 0x0800 && address < 0x1000) return "Default char ROM shadow"; + if (address >= 0xa000 && address < 0xc000) return "BASIC ROM / RAM"; + if (address >= 0xd000 && address < 0xd400) return "VIC-II registers"; + if (address >= 0xd400 && address < 0xd800) return "SID registers"; + if (address >= 0xd800 && address < 0xdc00) return "Color RAM"; + if (address >= 0xdc00 && address < 0xdd00) return "CIA1 registers"; + if (address >= 0xdd00 && address < 0xde00) return "CIA2 registers"; + if (address >= 0xe000) return "KERNAL ROM / RAM"; + return ""; +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..8467dc6 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./c64.js";