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