From 6c3cd146a3904838c7ab8d087303ee38e2c575e6 Mon Sep 17 00:00:00 2001 From: David Whitney Date: Sat, 18 Apr 2020 22:39:25 +0100 Subject: [PATCH] Hacking in config and stuff like that. Scratching out sound. --- CoreBoy/CoreBoy.csproj | 12 +- CoreBoy/GameboyOptions.cs | 83 +++++- CoreBoy/Program.cs | 60 ++-- CoreBoy/debugging/Console.cs | 114 ------- CoreBoy/debugging/ConsoleUtil.java | 11 - CoreBoy/debugging/command/Quit.java | 22 -- CoreBoy/debugging/command/ShowHelp.java | 74 ----- CoreBoy/debugging/command/apu/Channel.cs | 34 --- CoreBoy/debugging/command/cpu/ShowOpcode.java | 184 ------------ .../debugging/command/cpu/ShowOpcodes.java | 48 --- .../debugging/command/ppu/ShowBackground.java | 277 ------------------ CoreBoy/gui/Emulator.cs | 67 +---- CoreBoy/gui/WinFormsEmulatorSurface.cs | 19 +- CoreBoy/gui/WinSound.cs | 167 +++++++++++ self-contained.cmd | 1 + 15 files changed, 295 insertions(+), 878 deletions(-) delete mode 100644 CoreBoy/debugging/Console.cs delete mode 100644 CoreBoy/debugging/ConsoleUtil.java delete mode 100644 CoreBoy/debugging/command/Quit.java delete mode 100644 CoreBoy/debugging/command/ShowHelp.java delete mode 100644 CoreBoy/debugging/command/apu/Channel.cs delete mode 100644 CoreBoy/debugging/command/cpu/ShowOpcode.java delete mode 100644 CoreBoy/debugging/command/cpu/ShowOpcodes.java delete mode 100644 CoreBoy/debugging/command/ppu/ShowBackground.java create mode 100644 CoreBoy/gui/WinSound.cs create mode 100644 self-contained.cmd diff --git a/CoreBoy/CoreBoy.csproj b/CoreBoy/CoreBoy.csproj index 4db1db6..a6096d5 100644 --- a/CoreBoy/CoreBoy.csproj +++ b/CoreBoy/CoreBoy.csproj @@ -5,15 +5,11 @@ 8.0 netcoreapp3.1 CoreBoy - true CoreBoy CoreBoy.Program + true - - - - @@ -29,10 +25,8 @@ - - - - + + diff --git a/CoreBoy/GameboyOptions.cs b/CoreBoy/GameboyOptions.cs index 0870a6c..cd1ae15 100644 --- a/CoreBoy/GameboyOptions.cs +++ b/CoreBoy/GameboyOptions.cs @@ -1,39 +1,70 @@ using System; using System.Collections.Generic; using System.IO; +using CommandLine; namespace CoreBoy { public class GameboyOptions { - public FileInfo RomFile { get; } - public bool ForceDmg { get; } - public bool ForceCgb { get; } - public bool UseBootstrap { get; } - public bool DisableBatterySaves { get; } - public bool Debug { get; } - public bool Headless { get; } + public FileInfo? RomFile => string.IsNullOrWhiteSpace(Rom) ? null : new FileInfo(Rom); + + [Option('r', "rom", Required = false, HelpText = "Rom file.")] + public string Rom { get; set; } + + [Option('d', "force-dmg", Required = false, HelpText = "ForceDmg.")] + public bool ForceDmg { get; set; } + + [Option('c', "force-cgb", Required = false, HelpText = "ForceCgb.")] + public bool ForceCgb { get; set; } + + [Option('b', "use-bootstrap", Required = false, HelpText = "UseBootstrap.")] + public bool UseBootstrap { get; set; } + + [Option("disable-battery-saves", Required = false, HelpText = "disable-battery-saves.")] + public bool DisableBatterySaves { get; set; } + + [Option("debug", Required = false, HelpText = "Debug.")] + public bool Debug { get; set; } + + [Option("headless", Required = false, HelpText = "headless.")] + public bool Headless { get; set; } + + public bool ShowUi => !Headless; + public bool IsSupportBatterySaves() => !DisableBatterySaves; + public bool RomSpecified => !string.IsNullOrWhiteSpace(Rom); + + public GameboyOptions() + { + } + public GameboyOptions(FileInfo romFile) : this(romFile, new string[0], new string[0]) { } public GameboyOptions(FileInfo romFile, ICollection longParameters, ICollection shortParams) { - RomFile = romFile; + Rom = romFile.FullName; ForceDmg = longParameters.Contains("force-dmg") || shortParams.Contains("d"); ForceCgb = longParameters.Contains("force-cgb") || shortParams.Contains("c"); - if (ForceDmg && ForceCgb) - { - throw new ArgumentException("force-dmg and force-cgb options are can't be used together"); - } UseBootstrap = longParameters.Contains("use-bootstrap") || shortParams.Contains("b"); DisableBatterySaves = longParameters.Contains("disable-battery-saves") || shortParams.Contains("db"); Debug = longParameters.Contains("debug"); Headless = longParameters.Contains("headless"); + + Verify(); + } + + public void Verify() + { + if (ForceDmg && ForceCgb) + { + throw new ArgumentException("force-dmg and force-cgb options are can't be used together"); + } } public static void PrintUsage(TextWriter stream) @@ -50,6 +81,34 @@ public static void PrintUsage(TextWriter stream) stream.WriteLine(" --headless Start in the headless mode"); stream.Flush(); } + + public static GameboyOptions Parse(string[] args) + { + var parser = new Parser(cfg => + { + cfg.AutoHelp = true; + cfg.HelpWriter = Console.Out; + }); + + var result = parser.ParseArguments(args) + .WithParsed(o => { o.Verify(); }); + + + if (result is Parsed parsed) + { + if (args.Length == 1 && args[0].Contains(".gb")) + { + parsed.Value.Rom = args[0]; + } + + return parsed.Value; + } + else + { + Console.WriteLine("Failed to parsed!"); + return null; + } + } } } \ No newline at end of file diff --git a/CoreBoy/Program.cs b/CoreBoy/Program.cs index 8b806f2..0e63223 100644 --- a/CoreBoy/Program.cs +++ b/CoreBoy/Program.cs @@ -3,52 +3,56 @@ using System.Linq; using System.Threading; using System.Windows.Forms; +using CommandLine; using CoreBoy.gui; namespace CoreBoy { - static class Program + public static class Program { [STAThread] - static void Main(string[] args) + public static void Main(string[] args) { + var cancellation = new CancellationTokenSource(); + var arguments = GameboyOptions.Parse(args); + Application.SetCompatibleTextRenderingDefault(false); Application.SetHighDpiMode(HighDpiMode.SystemAware); Application.EnableVisualStyles(); - var cancellationTokenSource = new CancellationTokenSource(); - var token = cancellationTokenSource.Token; - - var arguments = new List(args); - - PromptForRom(arguments); - var emulator = new Emulator(arguments); - var ui = new WinFormsEmulatorSurface(); - emulator.Controller = ui; - emulator.Display.OnFrameProduced += ui.UpdateDisplay; - ui.Closed += (sender, e) => + if (!arguments.RomSpecified && arguments.ShowUi) { - cancellationTokenSource.Cancel(); - }; - - emulator.Run(token); - Application.Run(ui); - } - - private static void PromptForRom(List arguments) - { - if (arguments.Any()) return; + var (success, romPath) = WinFormsEmulatorSurface.PromptForRom(); + arguments.Rom = success ? romPath : string.Empty; + } - using var openFileDialog = new OpenFileDialog + if (!arguments.RomSpecified) + { + GameboyOptions.PrintUsage(Console.Out); + Console.Out.Flush(); + Environment.Exit(1); + } + + if (arguments.ShowUi) { - Filter = "Gameboy ROM (*.gb)|*.gb| All files(*.*) |*.*", FilterIndex = 0, RestoreDirectory = true - }; + var ui = new WinFormsEmulatorSurface(); + ui.Closed += (_, e) => { cancellation.Cancel(); }; + emulator.Controller = ui; + emulator.Display.OnFrameProduced += ui.UpdateDisplay; - if (openFileDialog.ShowDialog() == DialogResult.OK) + emulator.Run(cancellation.Token); + Application.Run(ui); + } + else { - arguments.Add(openFileDialog.FileName); + emulator.Run(cancellation.Token); + Console.WriteLine("Emulator running headless."); + Console.WriteLine("Press ANY key to exit."); + Console.ReadKey(true); + + cancellation.Cancel(); } } } diff --git a/CoreBoy/debugging/Console.cs b/CoreBoy/debugging/Console.cs deleted file mode 100644 index 7aee495..0000000 --- a/CoreBoy/debugging/Console.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Channels; - -namespace eu.rekawek.coffeegb.debug -{ - - public class Console - { - - // private static readonly Logger LOG = LoggerFactory.getLogger(Console.class); - - private readonly Deque commandBuffer = new ArrayDeque<>(); - - private readonly Semaphore semaphore = new Semaphore(0); - - private volatile bool isStarted; - - private List commands; - - public Console() - { - } - - public void init(Gameboy gameboy) - { - commands = new List(); - commands.add(new ShowHelp(commands)); - commands.add(new ShowOpcode()); - commands.add(new ShowOpcodes()); - commands.add(new Quit()); - - commands.add(new ShowBackground(gameboy, ShowBackground.Type.WINDOW)); - commands.add(new ShowBackground(gameboy, ShowBackground.Type.BACKGROUND)); - commands.add(new Channel(gameboy.getSound())); - - Collections.sort(commands, Comparator.comparing(c=>c.getPattern().getCommandNames().get(0))); - } - - - public void run() - { - isStarted = true; - - LineReader lineReader = LineReaderBuilder - .builder() - .build(); - - while (true) - { - try - { - String line = lineReader.readLine("coffee-gb> "); - foreach (Command cmd in commands) { - if (cmd.getPattern().matches(line)) - { - ParsedCommandLine parsed = cmd.getPattern().parse(line); - commandBuffer.add(new CommandExecution(cmd, parsed)); - semaphore.acquire(); - } - } - } - catch (IllegalArgumentException e) - { - System.err.println(e.getMessage()); - } - catch (UserInterruptException e) - { - System.exit(0); - } - catch (InterruptedException e) - { - //LOG.error("Interrupted", e); - break; - } - } - } - - public void tick() - { - if (!isStarted) - { - return; - } - - while (!commandBuffer.isEmpty()) - { - commandBuffer.poll().run(); - semaphore.release(); - } - } - - private class CommandExecution - { - - private readonly Command command; - - private readonly ParsedCommandLine arguments; - - public CommandExecution(Command command, ParsedCommandLine arguments) - { - this.command = command; - this.arguments = arguments; - } - - public void run() - { - command.run(arguments); - } - } - } - -} \ No newline at end of file diff --git a/CoreBoy/debugging/ConsoleUtil.java b/CoreBoy/debugging/ConsoleUtil.java deleted file mode 100644 index 3d77c8b..0000000 --- a/CoreBoy/debugging/ConsoleUtil.java +++ /dev/null @@ -1,11 +0,0 @@ -package eu.rekawek.coffeegb.debug; - -public final class ConsoleUtil { - - private ConsoleUtil() { - } - - public static void printSeparator(int width) { - System.out.println(String.format("%" + width + "s", "").replace(' ', '-')); - } -} diff --git a/CoreBoy/debugging/command/Quit.java b/CoreBoy/debugging/command/Quit.java deleted file mode 100644 index e746c90..0000000 --- a/CoreBoy/debugging/command/Quit.java +++ /dev/null @@ -1,22 +0,0 @@ -package eu.rekawek.coffeegb.debug.command; - -import eu.rekawek.coffeegb.debug.Command; -import eu.rekawek.coffeegb.debug.CommandPattern; - -public class Quit implements Command { - - private static final CommandPattern PATTERN = CommandPattern.Builder - .create("quit", "q") - .withDescription("quits the emulator") - .build(); - - @Override - public CommandPattern getPattern() { - return PATTERN; - } - - @Override - public void run(CommandPattern.ParsedCommandLine commandLine) { - System.exit(0); - } -} diff --git a/CoreBoy/debugging/command/ShowHelp.java b/CoreBoy/debugging/command/ShowHelp.java deleted file mode 100644 index 27683a9..0000000 --- a/CoreBoy/debugging/command/ShowHelp.java +++ /dev/null @@ -1,74 +0,0 @@ -package eu.rekawek.coffeegb.debug.command; - -import eu.rekawek.coffeegb.debug.Command; -import eu.rekawek.coffeegb.debug.CommandArgument; -import eu.rekawek.coffeegb.debug.CommandPattern; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import static com.google.common.collect.Maps.newHashMap; - -public class ShowHelp implements Command { - - private static final CommandPattern PATTERN = CommandPattern.Builder - .create("help", "?") - .withDescription("displays supported commands") - .build(); - - private final List commands; - - public ShowHelp(List commands) { - this.commands = commands; - } - - @Override - public CommandPattern getPattern() { - return PATTERN; - } - - @Override - public void run(CommandPattern.ParsedCommandLine commandLine) { - int max = 0; - Map commandMap = newHashMap(); - for (Command command : commands) { - CommandPattern pattern = command.getPattern(); - String alias = pattern.getCommandNames().get(0); - String commandWithArgs = getCommandWithArgs(alias, pattern.getArguments()); - if (commandWithArgs.length() > max) { - max = commandWithArgs.length(); - } - commandMap.put(command, commandWithArgs); - } - - for (Command command : commands) { - CommandPattern pattern = command.getPattern(); - String longName = commandMap.get(command); - System.out.print(String.format("%-" + max + "s", longName)); - if (pattern.getCommandNames().size() > 1) { - System.out.print(String.format(" %-5s", pattern.getCommandNames().get(1))); - } else { - System.out.print(" "); - } - command.getPattern() - .getDescription() - .map(d -> " " + d) - .ifPresent(System.out::print); - System.out.println(); - } - } - - private String getCommandWithArgs(String alias, List args) { - StringBuilder builder = new StringBuilder(alias); - if (!args.isEmpty()) { - builder.append(' ') - .append(String.join(" ", args - .stream() - .map(CommandArgument::toString) - .collect(Collectors.toList()))); - } - return builder.toString(); - } -} diff --git a/CoreBoy/debugging/command/apu/Channel.cs b/CoreBoy/debugging/command/apu/Channel.cs deleted file mode 100644 index 782caac..0000000 --- a/CoreBoy/debugging/command/apu/Channel.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; -using CoreBoy.sound; - -namespace CoreBoy.debugging.command.apu -{ - public class Channel : ICommand - { - private static readonly CommandPattern Pattern = CommandPattern.Builder - .Create("apu chan") - .WithDescription("enable given channels (1-4)") - .Build(); - - private readonly Sound _sound; - - public Channel(Sound sound) - { - _sound = sound; - } - - public CommandPattern GetPattern() - { - return Pattern; - } - - public void Run(CommandPattern.ParsedCommandLine commandLine) - { - var channels = new HashSet(commandLine.GetRemainingArguments()); - for (var i = 1; i <= 4; i++) - { - _sound.EnableChannel(i - 1, channels.Contains(i.ToString())); - } - } - } -} \ No newline at end of file diff --git a/CoreBoy/debugging/command/cpu/ShowOpcode.java b/CoreBoy/debugging/command/cpu/ShowOpcode.java deleted file mode 100644 index 2b37215..0000000 --- a/CoreBoy/debugging/command/cpu/ShowOpcode.java +++ /dev/null @@ -1,184 +0,0 @@ -package eu.rekawek.coffeegb.debug.command.cpu; - -import eu.rekawek.coffeegb.cpu.Opcodes; -import eu.rekawek.coffeegb.cpu.op.Op; -import eu.rekawek.coffeegb.cpu.opcode.Opcode; -import eu.rekawek.coffeegb.debug.Command; -import eu.rekawek.coffeegb.debug.CommandPattern; -import eu.rekawek.coffeegb.debug.CommandPattern.ParsedCommandLine; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.function.BiFunction; -import java.util.function.Predicate; - -import static eu.rekawek.coffeegb.debug.ConsoleUtil.printSeparator; - -public class ShowOpcode implements Command { - - private static final CommandPattern PATTERN = CommandPattern.Builder - .create("cpu show opcode") - .withRequiredArgument("opcode") - .withDescription("displays opcode information for hex (0xFA) or name (LD A,B) identifier") - .build(); - - @Override - public CommandPattern getPattern() { - return PATTERN; - } - - @Override - public void run(ParsedCommandLine commandLine) { - String arg; - if (commandLine.getRemainingArguments().isEmpty()) { - arg = commandLine.getArgument("opcode"); - } else { - arg = commandLine.getArgument("opcode") + " " + String.join(" ", commandLine.getRemainingArguments()); - } - - Opcode opcode = getOpcodeFromArg(arg); - if (opcode == null) { - System.out.println("Can't found opcode for " + arg); - return; - } - - boolean isExt = Opcodes.EXT_COMMANDS.get(opcode.getOpcode()) == opcode; - - List ops = new ArrayList<>(); - BiFunction addOp = (c, d) -> ops.add(new OpDescription(c, d)); - if (isExt) { - addOp.apply(4, "read opcode 0xCB"); - } - addOp.apply(4, String.format("read opcode 0x%02X", opcode.getOpcode())); - for (int i = 0; i < opcode.getOperandLength(); i++) { - addOp.apply(4, String.format("read operand %d", i + 1)); - } - opcode.getOps().stream().map(OpDescription::new).forEach(ops::add); - - List compacted = new ArrayList<>(); - for (int i = 0; i < ops.size(); i++) { - OpDescription o = ops.get(i); - if (o.description.equals("wait cycle")) { - if (!compacted.isEmpty()) { - compacted.get(compacted.size() - 1).updateCycles(4); - } - } else if (o.description.equals("finish cycle")) { - if (i < ops.size() - 1) { - OpDescription nextOp = ops.get(++i); - nextOp.updateCycles(4); - compacted.add(nextOp); - } - } else { - compacted.add(o); - } - } - - int stringLength = compacted - .stream() - .map(OpDescription::toString) - .map(String::length) - .mapToInt(Integer::valueOf) - .max() - .orElse(0); - - int totalCycles = compacted - .stream() - .mapToInt(o -> o.cycles) - .sum(); - - - int totalCyclesUntilCondition = compacted - .stream() - .filter(new Predicate() { - private boolean conditionalOccurred; - @Override - public boolean test(OpDescription opDescription) { - conditionalOccurred = conditionalOccurred || opDescription.description.startsWith("? "); - return !conditionalOccurred; - } - }) - .mapToInt(o -> o.cycles) - .sum(); - - if (isExt) { - System.out.println(String.format("0xCB%02X %s", opcode.getOpcode(), opcode.getLabel())); - } else { - System.out.println(String.format("0x%02X %s", opcode.getOpcode(), opcode.getLabel())); - } - printSeparator(stringLength); - compacted.forEach(System.out::println); - printSeparator(stringLength); - if (totalCyclesUntilCondition != totalCycles) { - System.out.println(String.format("Total cycles: %d / %d", totalCycles, totalCyclesUntilCondition)); - } else { - System.out.println(String.format("Total cycles: %d", totalCycles)); - } - } - - private Opcode getOpcodeFromArg(String arg) { - if (arg.toLowerCase().matches("0x[0-9a-f]{2}")) { - return getFromHex(Opcodes.COMMANDS, arg.substring(2)); - } else if (arg.toLowerCase().matches("0xcb[0-9a-f]{2}")) { - return getFromHex(Opcodes.EXT_COMMANDS, arg.substring(4)); - } else if (arg.toLowerCase().matches("[0-9a-f]{2}")) { - return getFromHex(Opcodes.COMMANDS, arg); - } else if (arg.toLowerCase().matches("cb[0-9a-f]{2}")) { - return getFromHex(Opcodes.EXT_COMMANDS, arg.substring(2)); - } - - String compactedArg = compactOpcodeLabel(arg); - Optional opcode = Opcodes.COMMANDS - .stream() - .filter(Objects::nonNull) - .filter(o -> compactedArg.equalsIgnoreCase(compactOpcodeLabel(o.getLabel()))) - .findFirst(); - if (!opcode.isPresent()) { - opcode = Opcodes.EXT_COMMANDS - .stream() - .filter(Objects::nonNull) - .filter(o -> compactedArg.equalsIgnoreCase(compactOpcodeLabel(o.getLabel()))) - .findFirst(); - } - return opcode.orElse(null); - } - - private Opcode getFromHex(List opcodes, String hexArg) { - return opcodes.get(Integer.parseInt(hexArg, 16)); - } - - private String compactOpcodeLabel(String label) { - return label - .replace(" ", "") - .toLowerCase(); - } - - public static class OpDescription { - - private final String description; - - private int cycles; - - public OpDescription(Op op) { - this.description = op.toString(); - if (op.writesMemory() || op.readsMemory()) { - this.cycles = 4; - } - } - - public OpDescription(int cycles, String description) { - this.description = description; - this.cycles = cycles; - } - - public void updateCycles(int cycles) { - this.cycles += cycles; - } - - @Override - public String toString() { - return String.format("%s %s", cycles == 0 ? " " : String.valueOf(cycles), description); - } - } -} diff --git a/CoreBoy/debugging/command/cpu/ShowOpcodes.java b/CoreBoy/debugging/command/cpu/ShowOpcodes.java deleted file mode 100644 index 3ebe797..0000000 --- a/CoreBoy/debugging/command/cpu/ShowOpcodes.java +++ /dev/null @@ -1,48 +0,0 @@ -package eu.rekawek.coffeegb.debug.command.cpu; - -import eu.rekawek.coffeegb.cpu.Opcodes; -import eu.rekawek.coffeegb.cpu.opcode.Opcode; -import eu.rekawek.coffeegb.debug.Command; -import eu.rekawek.coffeegb.debug.CommandPattern; -import eu.rekawek.coffeegb.debug.CommandPattern.ParsedCommandLine; - -import java.util.List; -import java.util.Objects; - -public class ShowOpcodes implements Command { - - private static final CommandPattern PATTERN = CommandPattern.Builder - .create("cpu show opcodes") - .withDescription("displays all opcodes") - .build(); - - @Override - public CommandPattern getPattern() { - return PATTERN; - } - - @Override - public void run(ParsedCommandLine commandLine) { - printTable(Opcodes.COMMANDS); - System.out.println("\n0xCB"); - printTable(Opcodes.EXT_COMMANDS); - } - - private static void printTable(List opcodes) { - System.out.print(" "); - for (int i = 0; i < 0x10; i++) { - System.out.print(String.format("%02X ", i)); - } - System.out.println(); - - for (int i = 0; i < 0x100; i += 0x10) { - System.out.print(String.format("%02X ", i)); - for (int j = 0; j < 0x10; j++) { - Opcode opcode = opcodes.get(i + j); - String label = opcode == null ? "-" : opcode.getLabel(); - System.out.print(String.format("%-12s", label)); - } - System.out.println(); - } - } -} diff --git a/CoreBoy/debugging/command/ppu/ShowBackground.java b/CoreBoy/debugging/command/ppu/ShowBackground.java deleted file mode 100644 index 27cbe86..0000000 --- a/CoreBoy/debugging/command/ppu/ShowBackground.java +++ /dev/null @@ -1,277 +0,0 @@ -package eu.rekawek.coffeegb.debug.command.ppu; - -import eu.rekawek.coffeegb.AddressSpace; -import eu.rekawek.coffeegb.Gameboy; -import eu.rekawek.coffeegb.debug.Command; -import eu.rekawek.coffeegb.debug.CommandPattern; -import eu.rekawek.coffeegb.debug.CommandPattern.ParsedCommandLine; -import eu.rekawek.coffeegb.gpu.Gpu; -import eu.rekawek.coffeegb.gpu.GpuRegister; -import eu.rekawek.coffeegb.gpu.Lcdc; -import eu.rekawek.coffeegb.gpu.TileAttributes; -import eu.rekawek.coffeegb.gui.SwingDisplay; -import eu.rekawek.coffeegb.memory.MemoryRegisters; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.awt.image.BufferedImage; - -import static eu.rekawek.coffeegb.cpu.BitUtils.toSigned; -import static eu.rekawek.coffeegb.gpu.Fetcher.zip; - -public class ShowBackground implements Command { - - private static final Logger LOG = LoggerFactory.getLogger(ShowBackground.class); - - public enum Type { - WINDOW, BACKGROUND; - } - - private static final CommandPattern PATTERN_BACKGROUND = CommandPattern.Builder - .create("ppu show background") - .withDescription("display the background tiles") - .build(); - - private static final CommandPattern PATTERN_WINDOW = CommandPattern.Builder - .create("ppu show window") - .withDescription("display the window tiles") - .build(); - - - private final Gameboy gameboy; - - private volatile boolean windowPresent; - - private final Type type; - - public ShowBackground(Gameboy gameboy, Type type) { - this.gameboy = gameboy; - this.type = type; - } - - @Override - public CommandPattern getPattern() { - return type == Type.WINDOW ? PATTERN_WINDOW : PATTERN_BACKGROUND; - } - - @Override - public void run(ParsedCommandLine commandLine) { - if (windowPresent) { - System.out.println("Window already present"); - return; - } - - SwingUtilities.invokeLater(() -> { - BackgroundTiles panel = new BackgroundTiles(gameboy.getGpu()); - panel.setPreferredSize(new Dimension(272 * 2, 272 * 2)); - - new Thread(panel).start(); - - Runnable panelTick = panel::tick; - - JFrame mainWindow = new JFrame(type == Type.BACKGROUND ? "Background tiles" : "Window tiles"); - mainWindow.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - mainWindow.setLocationRelativeTo(null); - mainWindow.addWindowListener(new WindowAdapter() { - @Override - public void windowClosed(WindowEvent e) { - super.windowClosed(e); - gameboy.unregisterTickListener(panelTick); - panel.doStop = true; - synchronized (panel) { - panel.notify(); - } - windowPresent = false; - } - }); - - mainWindow.setContentPane(panel); - mainWindow.setResizable(false); - mainWindow.setVisible(true); - mainWindow.pack(); - - gameboy.registerTickListener(panelTick); - - windowPresent = true; - }); - } - - public class BackgroundTiles extends JPanel implements Runnable { - - private boolean doRefresh; - - private boolean doStop; - - private final BufferedImage img; - - private final GraphicsConfiguration gfxConfig; - - private final Gpu gpu; - - private Gpu.Mode lastMode; - - public BackgroundTiles(Gpu gpu) { - super(); - gfxConfig = GraphicsEnvironment. - getLocalGraphicsEnvironment().getDefaultScreenDevice(). - getDefaultConfiguration(); - img = gfxConfig.createCompatibleImage(272 * 2, 272 * 2); - this.gpu = gpu; - } - - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); - Graphics2D g2d = (Graphics2D) g.create(); - g2d.drawImage(img, 0, 0, 272 * 2, 272 * 2, null); - g2d.dispose(); - } - - private void drawBackground() { - Graphics2D g2d = img.createGraphics(); - g2d.clearRect(0, 0, 272 * 2, 272 * 2); - - Lcdc lcdc = gpu.getLcdc(); - AddressSpace videoRam0 = gpu.getVideoRam0(); - AddressSpace videoRam1 = gpu.getVideoRam1(); - MemoryRegisters reg = gpu.getRegisters(); - - int tileMap = type == Type.BACKGROUND ? lcdc.getBgTileMapDisplay() : lcdc.getWindowTileMapDisplay(); - int tileData = lcdc.getBgWindowTileData(); - int dmgPalette = reg.get(GpuRegister.BGP); - - int[][] tile = new int[8][8]; - for (int x = 0; x < 32; x++) { - for (int y = 0; y < 32; y++) { - int tileId = videoRam0.getByte(tileMap + x + 32 * y); - TileAttributes attr; - if (videoRam1 == null) { - attr = TileAttributes.valueOf(0); - } else { - attr = TileAttributes.valueOf(videoRam1.getByte(tileMap + x + 32 * y)); - } - final int tileAddress; - if (lcdc.isBgWindowTileDataSigned()) { - tileAddress = tileData + toSigned(tileId) * 0x10; - } else { - tileAddress = tileData + tileId * 0x10; - } - for (int i = 0; i < 16; i += 2) { - AddressSpace bank = attr.getBank() == 0 ? videoRam0 : videoRam1; - int b1 = bank.getByte(tileAddress + i); - int b2 = bank.getByte(tileAddress + i + 1); - zip(b1, b2, attr.isXflip(), tile[i/2]); - } - if (attr.isYflip()) { - for (int i = 0; i < 8; i++) { - int[] tmp = tile[i]; - tile[i] = tile[7 - i]; - tile[7 - i] = tmp; - } - } - int[] palette = new int[4]; - if (gpu.isGbc()) { - int[] gbcPalette = gpu.getBgPalette().getPalette(attr.getColorPaletteIndex()); - for (int i = 0; i < palette.length; i++) { - palette[i] = SwingDisplay.translateGbcRgb(gbcPalette[i]); - } - } else { - for (int i = 0; i < palette.length; i++) { - palette[i] = SwingDisplay.COLORS[0b11 & (dmgPalette >> (i * 2))]; - } - } - drawTile(g2d, tile, palette, x, y); - } - } - if (type == Type.BACKGROUND) { - drawScrollFrame(g2d, reg.get(GpuRegister.SCX), reg.get(GpuRegister.SCY)); - } - } - - private void drawTile(Graphics2D g2d, int[][] tile, int[] palette, int x, int y) { - for (int i = 0; i < 8; i++) { - for (int j = 0; j < 8; j++) { - g2d.setColor(new Color(palette[tile[j][i]])); - g2d.fillRect(x * 17 + i * 2, y * 17 + j * 2, 2, 2); - } - } - } - - private void drawScrollFrame(Graphics2D g2d, int scx, int scy) { - g2d.setColor(Color.RED); - drawHorizontalLine(g2d, scx, scy); - drawVerticalLine(g2d, scx, scy); - drawVerticalLine(g2d, (scx + 160) % 256, scy); - drawHorizontalLine(g2d, scx, (scy + 144) % 256); - } - - private void drawHorizontalLine(Graphics2D g2d, int x1, int y1) { - if (x1 + 160 < 256) { - drawLine(g2d, x1, y1, x1 + 160, y1); - } else { - drawLine(g2d, x1, y1, 255, y1); - drawLine(g2d, 0, y1, x1 + 160 - 256, y1); - } - } - - private void drawVerticalLine(Graphics2D g2d, int x1, int y1) { - if (y1 + 144 < 256) { - drawLine(g2d, x1, y1, x1, y1 + 144); - } else { - drawLine(g2d, x1, y1, x1, 255); - drawLine(g2d, x1, 0, x1 , y1+ 144 - 256); - } - } - - private void drawLine(Graphics2D g2d, int x1, int y1, int x2, int y2) { - g2d.drawLine(translate(x1), translate(y1), translate(x2), translate(y2)); - if (x1 == x2) { - g2d.drawLine(translate(x1) + 1, translate(y1), translate(x2) + 1, translate(y2)); - } - if (y1 == y2) { - g2d.drawLine(translate(x1), translate(y1) + 1, translate(x2), translate(y2) + 1); - } - } - - private int translate(int point) { - return point / 8 * 17 + (point % 8) * 2; - } - - @Override - public void run() { - while (!doStop) { - synchronized (this) { - try { - wait(); - } catch (InterruptedException e) { - LOG.error("Can't refresh background window", e); - return; - } - } - - if (doRefresh) { - doRefresh = false; - validate(); - repaint(); - } - } - } - - public void tick() { - Gpu.Mode currentMode = gpu.getMode(); - if (currentMode != lastMode && currentMode == Gpu.Mode.VBlank) { - drawBackground(); - doRefresh = true; - synchronized (this) { - notify(); - } - } - lastMode = currentMode; - } - } - -} \ No newline at end of file diff --git a/CoreBoy/gui/Emulator.cs b/CoreBoy/gui/Emulator.cs index 78a7327..a6cb832 100644 --- a/CoreBoy/gui/Emulator.cs +++ b/CoreBoy/gui/Emulator.cs @@ -22,73 +22,19 @@ public class Emulator: IRunnable private readonly List _runnables; - public Emulator(IEnumerable args) + public Emulator(GameboyOptions options) { _runnables = new List(); - Options = ParseArgs(args.ToArray()); + Options = options; } - - private static GameboyOptions ParseArgs(string[] args) - { - if (args.Length == 0) - { - GameboyOptions.PrintUsage(Console.Out); - Environment.Exit(0); - return null; - } - try - { - return CreateGameboyOptions(args); - } - catch (ArgumentException e) - { - Console.Error.WriteLine(e.Message); - Console.Error.WriteLine(); - GameboyOptions.PrintUsage(Console.Error); - Environment.Exit(1); - return null; - } - } - - private static GameboyOptions CreateGameboyOptions(string[] args) + public void Run(CancellationToken token) { - var longParams = new HashSet(); - var shortParams = new HashSet(); - - string romPath = null; - foreach (var a in args) + if (!Options.RomSpecified || !Options.RomFile.Exists) { - if (a.StartsWith("--")) - { - longParams.Add(a.Substring(2)); - } - else if (a.StartsWith("-")) - { - shortParams.Add(a.Substring(1)); - } - else - { - romPath = a; - } - } - - if (romPath == null) - { - throw new ArgumentException("ROM path hasn't been specified"); - } - - var romFile = new FileInfo(romPath); - if (!romFile.Exists) - { - throw new ArgumentException("The ROM path doesn't exist: " + romPath); + throw new ArgumentException("The ROM path doesn't exist: " + Options.RomFile); } - return new GameboyOptions(romFile, longParams, shortParams); - } - - public void Run(CancellationToken token) - { var rom = new Cartridge(Options); Gameboy = CreateGameboy(rom); @@ -128,8 +74,7 @@ private Gameboy CreateGameboy(Cartridge rom) //controller = new SwingController(properties); //gameboy = new Gameboy(options, rom, display, controller, sound, serialEndpoint, console); - return new Gameboy(Options, rom, Display, Controller, new NullSoundOutput(), SerialEndpoint); + return new Gameboy(Options, rom, Display, Controller, new WinSound(), SerialEndpoint); } } - } \ No newline at end of file diff --git a/CoreBoy/gui/WinFormsEmulatorSurface.cs b/CoreBoy/gui/WinFormsEmulatorSurface.cs index ae920ae..5b93450 100644 --- a/CoreBoy/gui/WinFormsEmulatorSurface.cs +++ b/CoreBoy/gui/WinFormsEmulatorSurface.cs @@ -46,6 +46,20 @@ public WinFormsEmulatorSurface() Controls.Add(_pictureBox); } + public static (bool, string) PromptForRom() + { + using var openFileDialog = new OpenFileDialog + { + Filter = "Gameboy ROM (*.gb)|*.gb| All files(*.*) |*.*", + FilterIndex = 0, + RestoreDirectory = true + }; + + return openFileDialog.ShowDialog() == DialogResult.OK + ? (true, openFileDialog.FileName) + : (false, null); + } + private void WinFormsEmulatorSurface_KeyDown(object sender, KeyEventArgs e) { var button = _controls.ContainsKey(e.KeyCode) ? _controls[e.KeyCode] : null; @@ -64,10 +78,7 @@ private void WinFormsEmulatorSurface_KeyUp(object sender, KeyEventArgs e) } } - public void SetButtonListener(IButtonListener listener) - { - _listener = listener; - } + public void SetButtonListener(IButtonListener listener) => _listener = listener; protected override void OnResize(EventArgs e) { diff --git a/CoreBoy/gui/WinSound.cs b/CoreBoy/gui/WinSound.cs new file mode 100644 index 0000000..1f5254f --- /dev/null +++ b/CoreBoy/gui/WinSound.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CoreBoy.sound; +using NAudio.Wave; +using NAudio.Wave.SampleProviders; + +namespace CoreBoy.gui +{ + + public class WinSound : ISoundOutput + { + private int _tick; + private readonly int _divider; + private AudioPlaybackEngine _engine; + + private const int SampleRate = 22050; + + public WinSound() + { + _divider = (int)(Gameboy.TicksPerSec / SampleRate); + } + + public void Start() + { + _engine = new AudioPlaybackEngine(SampleRate, 2); + } + + public void Stop() + { + _engine?.Dispose(); + } + + public void Play(int left, int right) + { + if (_tick++ != 0) + { + _tick %= _divider; + return; + } + + //Beep((uint)left, 5);*/ + } + } + + public class AudioPlaybackEngine : IDisposable + { + private readonly IWavePlayer _outputDevice; + private readonly MixingSampleProvider _mixer; + + public AudioPlaybackEngine(int sampleRate = 44100, int channelCount = 2) + { + _outputDevice = new WaveOutEvent(); + _mixer = new MixingSampleProvider(WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channelCount)) + { + ReadFully = true + }; + + _outputDevice.Init(_mixer); + _outputDevice.Play(); + } + + public void PlaySound(string fileName) + { + var input = new AudioFileReader(fileName); + AddMixerInput(new AutoDisposeFileReader(input)); + } + + public void PlaySound(CachedSound sound) + { + AddMixerInput(new CachedSoundSampleProvider(sound)); + } + + private void AddMixerInput(ISampleProvider input) + { + _mixer.AddMixerInput(ConvertToRightChannelCount(input)); + } + + private ISampleProvider ConvertToRightChannelCount(ISampleProvider input) + { + if (input.WaveFormat.Channels == _mixer.WaveFormat.Channels) + { + return input; + } + if (input.WaveFormat.Channels == 1 && _mixer.WaveFormat.Channels == 2) + { + return new MonoToStereoSampleProvider(input); + } + throw new NotImplementedException("Not yet implemented this channel count conversion"); + } + + public void Dispose() + { + _outputDevice.Dispose(); + } + } + + public class AutoDisposeFileReader : ISampleProvider + { + private readonly AudioFileReader _reader; + private bool _isDisposed; + public AutoDisposeFileReader(AudioFileReader reader) + { + this._reader = reader; + this.WaveFormat = reader.WaveFormat; + } + + public int Read(float[] buffer, int offset, int count) + { + if (_isDisposed) + return 0; + + var read = _reader.Read(buffer, offset, count); + if (read == 0) + { + _reader.Dispose(); + _isDisposed = true; + } + return read; + } + + public WaveFormat WaveFormat { get; private set; } + } + public class CachedSoundSampleProvider : ISampleProvider + { + private readonly CachedSound _cachedSound; + private long _position; + + public CachedSoundSampleProvider(CachedSound cachedSound) + { + this._cachedSound = cachedSound; + } + + public int Read(float[] buffer, int offset, int count) + { + var availableSamples = _cachedSound.AudioData.Length - _position; + var samplesToCopy = Math.Min(availableSamples, count); + Array.Copy(_cachedSound.AudioData, _position, buffer, offset, samplesToCopy); + _position += samplesToCopy; + return (int)samplesToCopy; + } + + public WaveFormat WaveFormat { get { return _cachedSound.WaveFormat; } } + } + + public class CachedSound + { + public float[] AudioData { get; private set; } + public WaveFormat WaveFormat { get; private set; } + public CachedSound(string audioFileName) + { + using (var audioFileReader = new AudioFileReader(audioFileName)) + { + // TODO: could add resampling in here if required + WaveFormat = audioFileReader.WaveFormat; + var wholeFile = new List((int)(audioFileReader.Length / 4)); + var readBuffer = new float[audioFileReader.WaveFormat.SampleRate * audioFileReader.WaveFormat.Channels]; + int samplesRead; + while ((samplesRead = audioFileReader.Read(readBuffer, 0, readBuffer.Length)) > 0) + { + wholeFile.AddRange(readBuffer.Take(samplesRead)); + } + AudioData = wholeFile.ToArray(); + } + } + } +} \ No newline at end of file diff --git a/self-contained.cmd b/self-contained.cmd new file mode 100644 index 0000000..f0ba58a --- /dev/null +++ b/self-contained.cmd @@ -0,0 +1 @@ +dotnet publish -c Release -r win10-x64 \ No newline at end of file