| 1 | package com.rox.debug.report; | |
| 2 | ||
| 3 | import com.rox.ByteUtil; | |
| 4 | import com.rox.cpu.mos6502.MOS6502Snapshot; | |
| 5 | import com.rox.cpu.mos6502.assembler.DisassembledInstruction; | |
| 6 | import com.rox.debug.cpu.CpuStateText; | |
| 7 | import com.rox.debug.memory.HexDump; | |
| 8 | import com.rox.ppu.PpuSnapshot; | |
| 9 | import com.rox.save.SaveFileFormat; | |
| 10 | import com.rox.save.SaveMetadata; | |
| 11 | import com.rox.save.SaveType; | |
| 12 | import com.rox.save.SystemSnapshot; | |
| 13 | ||
| 14 | import javax.imageio.ImageIO; | |
| 15 | import java.awt.image.BufferedImage; | |
| 16 | import java.io.IOException; | |
| 17 | import java.nio.charset.StandardCharsets; | |
| 18 | import java.nio.file.Files; | |
| 19 | import java.nio.file.Path; | |
| 20 | import java.time.ZoneId; | |
| 21 | import java.time.format.DateTimeFormatter; | |
| 22 | import java.util.Map; | |
| 23 | ||
| 24 | /** | |
| 25 | * Writes a {@link DebugReport} as a folder of files a person (or an AI assistant) can dig through: | |
| 26 | * <ul> | |
| 27 | * <li>{@code screenshot.png} - the last complete frame (absent if none had been shown yet)</li> | |
| 28 | * <li>{@code framebuffer-partial.png} - the PPU's part-drawn frame at the moment of capture</li> | |
| 29 | * <li>{@code description.txt} - what the person flagging the issue saw</li> | |
| 30 | * <li>{@code state.txt} - CPU registers/flags, the next instructions, PPU and mapper state</li> | |
| 31 | * <li>{@code ram.bin}/{@code ram.hex.txt} - the 2KB of CPU RAM ({@code $0000-$07FF}, mirrored through {@code $1FFF})</li> | |
| 32 | * <li>{@code oam.bin}, {@code nametables.bin}, {@code palette.bin}, {@code prg-ram.bin}, and | |
| 33 | * {@code chr-ram.bin} (CHR-RAM boards only) - raw dumps</li> | |
| 34 | * <li>{@code <folder name>.sav} - the restorable {@link SystemSnapshot}</li> | |
| 35 | * </ul> | |
| 36 | */ | |
| 37 | public final class DebugReportWriter { | |
| 38 | private static final int SCREEN_WIDTH = 256; | |
| 39 | private static final int SCREEN_HEIGHT = 240; | |
| 40 | private static final DateTimeFormatter HEADER_TIMESTAMP = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); | |
| 41 | ||
| 42 | private DebugReportWriter(){ | |
| 43 | } | |
| 44 | ||
| 45 | /** Writes every file into {@code directory} (created if needed), naming the {@code .sav} after the directory. */ | |
| 46 | public static void write(final Path directory, final DebugReport report) throws IOException { | |
| 47 | Files.createDirectories(directory); | |
| 48 | final SystemSnapshot snapshot = report.snapshot(); | |
| 49 | final PpuSnapshot ppu = snapshot.ppu(); | |
| 50 | ||
| 51 |
1
1. write : negated conditional → KILLED |
if (report.screenshotRgb() != null){ |
| 52 |
1
1. write : removed call to com/rox/debug/report/DebugReportWriter::writePng → KILLED |
writePng(directory.resolve("screenshot.png"), report.screenshotRgb()); |
| 53 | } | |
| 54 |
1
1. write : removed call to com/rox/debug/report/DebugReportWriter::writePng → KILLED |
writePng(directory.resolve("framebuffer-partial.png"), report.partialFrameRgb()); |
| 55 | Files.writeString(directory.resolve("description.txt"), report.description(), StandardCharsets.UTF_8); | |
| 56 | Files.writeString(directory.resolve("state.txt"), formatState(report), StandardCharsets.UTF_8); | |
| 57 | ||
| 58 | final int[] ram = snapshot.ram(); | |
| 59 | Files.write(directory.resolve("ram.bin"), ByteUtil.toBytes(ram)); | |
| 60 | Files.writeString(directory.resolve("ram.hex.txt"), hexDump(ram), StandardCharsets.UTF_8); | |
| 61 | Files.write(directory.resolve("oam.bin"), ByteUtil.toBytes(ppu.oam())); | |
| 62 | Files.write(directory.resolve("nametables.bin"), ByteUtil.toBytes(ppu.nametableRam())); | |
| 63 | Files.write(directory.resolve("palette.bin"), ByteUtil.toBytes(ppu.paletteRam())); | |
| 64 | Files.write(directory.resolve("prg-ram.bin"), ByteUtil.toBytes(snapshot.mapper().prgRam())); | |
| 65 |
2
1. write : changed conditional boundary → KILLED 2. write : negated conditional → KILLED |
if (snapshot.mapper().chrRam().length > 0){ |
| 66 | Files.write(directory.resolve("chr-ram.bin"), ByteUtil.toBytes(snapshot.mapper().chrRam())); | |
| 67 | } | |
| 68 | ||
| 69 | final SaveMetadata metadata = new SaveMetadata(SaveType.LIVE_SNAPSHOT, | |
| 70 | report.capturedAt().atZone(ZoneId.systemDefault()).toInstant(), 0); | |
| 71 |
1
1. write : removed call to com/rox/save/SaveFileFormat::writeLiveSnapshot → KILLED |
SaveFileFormat.writeLiveSnapshot(directory.resolve(directory.getFileName() + ".sav"), metadata, snapshot); |
| 72 | } | |
| 73 | ||
| 74 | static String formatState(final DebugReport report){ | |
| 75 | final MOS6502Snapshot cpu = report.snapshot().cpu(); | |
| 76 | final PpuSnapshot ppu = report.snapshot().ppu(); | |
| 77 | final StringBuilder out = new StringBuilder(); | |
| 78 | out.append("EmuRoxLab debug snapshot\n"); | |
| 79 | out.append("ROM: ").append(report.romName()).append('\n'); | |
| 80 | out.append("Captured: ").append(HEADER_TIMESTAMP.format(report.capturedAt())).append("\n\n"); | |
| 81 | ||
| 82 | out.append("--- CPU ---\n"); | |
| 83 | out.append(CpuStateText.registers(cpu)).append('\n'); | |
| 84 | out.append("Flags: ").append(CpuStateText.flags(cpu)).append(" (NV-BDIZC, uppercase = set)\n"); | |
| 85 | out.append(CpuStateText.interruptLines(cpu)).append("\n\n"); | |
| 86 | ||
| 87 | out.append("--- Next instructions ---\n"); | |
| 88 | boolean first = true; | |
| 89 | for (final DisassembledInstruction instruction : report.upcomingInstructions()){ | |
| 90 | out.append(CpuStateText.instruction(instruction, first)).append('\n'); | |
| 91 | first = false; | |
| 92 | } | |
| 93 | out.append('\n'); | |
| 94 | ||
| 95 | final PpuSnapshot.Timing timing = ppu.timing(); | |
| 96 | final PpuSnapshot.Registers registers = ppu.registers(); | |
| 97 | out.append("--- PPU ---\n"); | |
| 98 | out.append(String.format("Scanline: %d Dot: %d VBlank: %s%n", timing.scanline(), timing.dot(), yesNo(timing.vblankFlag()))); | |
| 99 | out.append(String.format("PPUCTRL: $%02X PPUMASK: $%02X OAMADDR: $%02X%n", | |
| 100 | registers.control(), registers.mask(), registers.oamAddress())); | |
| 101 | out.append(String.format("v: $%04X t: $%04X fine X: %d write toggle: %d%n", registers.currentVramAddress(), | |
| 102 |
1
1. formatState : negated conditional → KILLED |
registers.temporaryVramAddress(), registers.fineXScroll(), registers.writeToggle() ? 1 : 0)); |
| 103 | out.append("Sprite overflow: ").append(yesNo(ppu.sprites().spriteOverflow())) | |
| 104 | .append(" Sprite 0 hit: ").append(yesNo(ppu.sprites().spriteZeroHitFlag())).append("\n\n"); | |
| 105 | ||
| 106 | out.append("--- Mapper ---\n"); | |
| 107 |
1
1. formatState : negated conditional → KILLED |
if (report.mapperState().isEmpty()){ |
| 108 | out.append("(fixed mapping, no bank registers)\n"); | |
| 109 | } | |
| 110 | for (final Map.Entry<String, String> entry : report.mapperState().entrySet()){ | |
| 111 | out.append(entry.getKey()).append(": ").append(entry.getValue()).append('\n'); | |
| 112 | } | |
| 113 |
1
1. formatState : replaced return value with "" for com/rox/debug/report/DebugReportWriter::formatState → KILLED |
return out.toString(); |
| 114 | } | |
| 115 | ||
| 116 | private static String yesNo(final boolean value){ | |
| 117 |
2
1. yesNo : negated conditional → KILLED 2. yesNo : replaced return value with "" for com/rox/debug/report/DebugReportWriter::yesNo → KILLED |
return value ? "yes" : "no"; |
| 118 | } | |
| 119 | ||
| 120 | /** 16 bytes per row, each row prefixed with its address: {@code 0010: 00 01 ... 0F}. */ | |
| 121 | static String hexDump(final int[] bytes){ | |
| 122 |
3
1. hexDump : replaced return value with "" for com/rox/debug/report/DebugReportWriter::hexDump → KILLED 2. lambda$hexDump$0 : replaced int return with 0 for com/rox/debug/report/DebugReportWriter::lambda$hexDump$0 → KILLED 3. hexDump : Replaced integer subtraction with addition → KILLED |
return HexDump.format(address -> bytes[address], 0, bytes.length - 1, false); |
| 123 | } | |
| 124 | ||
| 125 | private static void writePng(final Path path, final int[] rgb) throws IOException { | |
| 126 | final BufferedImage image = new BufferedImage(SCREEN_WIDTH, SCREEN_HEIGHT, BufferedImage.TYPE_INT_RGB); | |
| 127 |
1
1. writePng : removed call to java/awt/image/BufferedImage::setRGB → KILLED |
image.setRGB(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, rgb, 0, SCREEN_WIDTH); |
| 128 | ImageIO.write(image, "png", path.toFile()); | |
| 129 | } | |
| 130 | } | |
Mutations | ||
| 51 |
1.1 |
|
| 52 |
1.1 |
|
| 54 |
1.1 |
|
| 65 |
1.1 2.2 |
|
| 71 |
1.1 |
|
| 102 |
1.1 |
|
| 107 |
1.1 |
|
| 113 |
1.1 |
|
| 117 |
1.1 2.2 |
|
| 122 |
1.1 2.2 3.3 |
|
| 127 |
1.1 |