Commit 091d6babd52d8bd01db863115f7da97dabdf9a85
Commits[COMMIT BEGIN]commit 091d6babd52d8bd01db863115f7da97dabdf9a85 Author: 0x4248 <[email protected]> Date: Fri Mar 6 21:59:24 2026 +0000 nova: init diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/BIOS.java b/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/BIOS.java deleted file mode 100644 index ecbef49..0000000 --- a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/BIOS.java +++ /dev/null @@ -1,237 +0,0 @@ -package com.github._0x4248.nova.BIOS; - -import com.github._0x4248.nova.BIOS.machines.Machine; -import com.github._0x4248.nova.BIOS.machines.StandardMachine; - -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import java.util.jar.Attributes; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; - -public class BIOS { - private static volatile BiosRuntime activeRuntime; - private static final int LOG_FOREGROUND = 15; - private static final int LOG_BACKGROUND = 1; - private static final int HEADER_FOREGROUND = 14; - private static final DateTimeFormatter CLOCK_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss"); - - private final Machine machine; - private final BiosRuntime runtime; - private final List<String> logLines; - private final int maxLogLines; - - public BIOS() { - this(new StandardMachine()); - } - - public BIOS(Machine machine) { - this.machine = machine; - this.runtime = new BiosRuntime(machine); - activeRuntime = this.runtime; - this.logLines = new ArrayList<>(); - this.maxLogLines = 22; - - initializeScreen(); - } - - public static BiosRuntime getRuntime() { - return activeRuntime; - } - - public boolean bootApplication(Path applicationJarPath, String[] applicationArgs) { - if (applicationJarPath == null) { - log("Boot failed: no application path provided."); - return false; - } - - if (!Files.exists(applicationJarPath) || !Files.isRegularFile(applicationJarPath)) { - log("Boot failed: application not found at " + applicationJarPath.toAbsolutePath()); - return false; - } - - log("Boot device detected: " + applicationJarPath.getFileName()); - - String mainClassName; - try { - mainClassName = resolveMainClass(applicationJarPath); - } catch (IOException e) { - log("Boot failed: cannot read application jar metadata."); - return false; - } - - if (mainClassName == null || mainClassName.isBlank()) { - log("Boot failed: no boot entry found (manifest Main-Class, Boot, or Main)."); - return false; - } - - log("Entrypoint: " + mainClassName); - log("Handing off control to application..."); - runtime.beep(70, 720); - - URL jarUrl; - try { - jarUrl = applicationJarPath.toUri().toURL(); - } catch (IOException e) { - log("Boot failed: invalid application jar URL."); - return false; - } - - try (URLClassLoader classLoader = new URLClassLoader(new URL[]{jarUrl}, BIOS.class.getClassLoader())) { - invokeMainClass(mainClassName, classLoader, applicationArgs); - System.out.println("[BIOS] Application exited."); - return true; - } catch (ClassNotFoundException e) { - log("Boot failed: main class not found: " + mainClassName); - return false; - } catch (NoSuchMethodException e) { - log("Boot failed: class has no main(String[] args): " + mainClassName); - return false; - } catch (IllegalAccessException e) { - log("Boot failed: cannot access main method: " + mainClassName); - return false; - } catch (InvocationTargetException e) { - log("Application crashed: " + e.getTargetException()); - return false; - } catch (IOException e) { - log("Boot failed: cannot open classloader."); - return false; - } - } - - public boolean bootInternalApplication(String className, String[] applicationArgs) { - if (className == null || className.isBlank()) { - log("Boot failed: no internal class name provided."); - return false; - } - - log("Boot source: internal ROM application"); - log("Entrypoint: " + className); - log("Handing off control to application..."); - runtime.beep(70, 720); - - try { - invokeMainClass(className, BIOS.class.getClassLoader(), applicationArgs); - System.out.println("[BIOS] Application exited."); - return true; - } catch (ClassNotFoundException e) { - log("Boot failed: main class not found: " + className); - return false; - } catch (NoSuchMethodException e) { - log("Boot failed: class has no main(String[] args): " + className); - return false; - } catch (IllegalAccessException e) { - log("Boot failed: cannot access main method: " + className); - return false; - } catch (InvocationTargetException e) { - log("Application crashed: " + e.getTargetException()); - return false; - } - } - - private void initializeScreen() { - runtime.clear(LOG_BACKGROUND); - runtime.drawText(8, 8, machine.biosLabel, HEADER_FOREGROUND, LOG_BACKGROUND, false); - runtime.drawText(8, 20, "Boot sequence start", LOG_FOREGROUND, LOG_BACKGROUND, false); - present(); - runtime.beep(60, 520); - } - - private void log(String message) { - String timestamp = LocalTime.now().format(CLOCK_FORMAT); - String line = "[" + timestamp + "] " + message; - System.out.println("[BIOS] " + message); - - logLines.add(line); - if (logLines.size() > maxLogLines) { - logLines.remove(0); - } - - renderLogScreen(); - present(); - } - - private void renderLogScreen() { - runtime.clear(LOG_BACKGROUND); - runtime.drawText(8, 8, machine.biosLabel, HEADER_FOREGROUND, LOG_BACKGROUND, false); - runtime.drawText(8, 20, "Boot log", HEADER_FOREGROUND, LOG_BACKGROUND, false); - - int y = 36; - int maxCharsPerLine = Math.max(1, runtime.getTextColumns() - 2); - for (String line : logLines) { - String clipped = line.length() > maxCharsPerLine ? line.substring(0, maxCharsPerLine) : line; - runtime.drawText(8, y, clipped, LOG_FOREGROUND, LOG_BACKGROUND, false); - y += 8; - } - } - - private void present() { - runtime.present(); - } - - private void invokeMainClass(String className, ClassLoader classLoader, String[] applicationArgs) - throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { - Class<?> mainClass = Class.forName(className, true, classLoader); - Method mainMethod = mainClass.getMethod("main", String[].class); - - int modifiers = mainMethod.getModifiers(); - if (!Modifier.isPublic(modifiers) || !Modifier.isStatic(modifiers)) { - throw new IllegalAccessException("main method must be public static"); - } - - mainMethod.invoke(null, (Object) (applicationArgs == null ? new String[0] : applicationArgs)); - } - - private String resolveMainClass(Path applicationJarPath) throws IOException { - try (JarFile jarFile = new JarFile(applicationJarPath.toFile())) { - if (jarFile.getManifest() != null) { - Attributes mainAttributes = jarFile.getManifest().getMainAttributes(); - String manifestMainClass = mainAttributes.getValue(Attributes.Name.MAIN_CLASS); - if (manifestMainClass != null && !manifestMainClass.isBlank()) { - return manifestMainClass.trim(); - } - } - - String bootClass = null; - String mainClass = null; - - Enumeration<JarEntry> entries = jarFile.entries(); - while (entries.hasMoreElements()) { - JarEntry entry = entries.nextElement(); - String name = entry.getName(); - if (!name.endsWith(".class") || name.contains("$") || name.startsWith("META-INF/")) { - continue; - } - - String className = name.substring(0, name.length() - 6).replace('/', '.'); - String simpleName = className.substring(className.lastIndexOf('.') + 1); - - if (simpleName.equalsIgnoreCase("Boot")) { - bootClass = className; - break; - } - - if (mainClass == null && simpleName.equalsIgnoreCase("Main")) { - mainClass = className; - } - } - - if (bootClass != null) { - return bootClass; - } - - return mainClass; - } - } -} diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/BiosRuntime.java b/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/BiosRuntime.java deleted file mode 100644 index fae6889..0000000 --- a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/BiosRuntime.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.github._0x4248.nova.BIOS; - -import com.github._0x4248.nova.BIOS.VGA.VGA; -import com.github._0x4248.nova.BIOS.machines.Machine; -import com.github._0x4248.nova.Core.Gui; - -public class BiosRuntime { - - private final Machine machine; - private final VGA vga; - private final Gui screen; - - public BiosRuntime(Machine machine) { - this.machine = machine; - this.vga = new VGA(); - this.screen = new Gui(vga.getWidth() * machine.screenScale, vga.getHeight() * machine.screenScale); - } - - public Machine getMachine() { - return machine; - } - - public int getWidth() { - return vga.getWidth(); - } - - public int getHeight() { - return vga.getHeight(); - } - - public int getTextColumns() { - return getWidth() / 8; - } - - public void clear(int colorIndex) { - vga.clear(colorIndex); - } - - public void drawText(int x, int y, String text, int foregroundColor, int backgroundColor, boolean transparentBackground) { - vga.drawText(x, y, text, foregroundColor, backgroundColor, transparentBackground); - } - - public void setPixel(int x, int y, int colorIndex) { - vga.setPixel(x, y, colorIndex); - } - - public void present() { - vga.blitToGui(screen, machine.screenScale); - } - - public void beep(int lengthMs, int pitchHz) { - if (!machine.supportsSound) { - return; - } - screen.biosBeep(lengthMs, pitchHz); - } - - public boolean hasKeyPress() { - if (!machine.supportsKeyboard) { - return false; - } - return screen.hasKeyPress(); - } - - public Integer pollKeyCode() { - if (!machine.supportsKeyboard) { - return null; - } - return screen.pollKeyCode(); - } -} diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/VGA/VGA.java b/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/VGA/VGA.java deleted file mode 100644 index cfa996d..0000000 --- a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/VGA/VGA.java +++ /dev/null @@ -1,193 +0,0 @@ -package com.github._0x4248.nova.BIOS.VGA; - -import com.github._0x4248.nova.Core.Gui; - -import java.awt.image.BufferedImage; -import java.util.Arrays; - -public class VGA { - - public static final int MODE13H_WIDTH = 320; - public static final int MODE13H_HEIGHT = 200; - - private final int width; - private final int height; - private final byte[] framebuffer; - private final int[] palette; - - public VGA() { - this(MODE13H_WIDTH, MODE13H_HEIGHT); - } - - public VGA(int width, int height) { - if (width <= 0 || height <= 0) { - throw new IllegalArgumentException("Width and height must be positive"); - } - - this.width = width; - this.height = height; - this.framebuffer = new byte[width * height]; - this.palette = new int[256]; - - initializeDefaultPalette(); - } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public void clear(int colorIndex) { - Arrays.fill(framebuffer, toPaletteIndex(colorIndex)); - } - - public void setPixel(int x, int y, int colorIndex) { - if (x < 0 || y < 0 || x >= width || y >= height) { - return; - } - - framebuffer[y * width + x] = toPaletteIndex(colorIndex); - } - - public int getPixel(int x, int y) { - if (x < 0 || y < 0 || x >= width || y >= height) { - return 0; - } - - return framebuffer[y * width + x] & 0xFF; - } - - public void drawChar(int x, int y, char character, int foregroundColor, int backgroundColor, boolean transparentBackground) { - int glyphIndex = character & 0x7F; - if (glyphIndex >= VGAFonts.LATIN8x8.length) { - glyphIndex = '?'; - } - - byte[] glyph = VGAFonts.LATIN8x8[glyphIndex]; - - for (int row = 0; row < 8; row++) { - int rowBits = glyph[row] & 0xFF; - - for (int col = 0; col < 8; col++) { - boolean on = ((rowBits >> col) & 1) == 1; - if (on) { - setPixel(x + col, y + row, foregroundColor); - } else if (!transparentBackground) { - setPixel(x + col, y + row, backgroundColor); - } - } - } - } - - public void drawText(int x, int y, String text, int foregroundColor, int backgroundColor, boolean transparentBackground) { - int cursorX = x; - int cursorY = y; - - for (int i = 0; i < text.length(); i++) { - char character = text.charAt(i); - - if (character == '\n') { - cursorX = x; - cursorY += 8; - continue; - } - - drawChar(cursorX, cursorY, character, foregroundColor, backgroundColor, transparentBackground); - cursorX += 8; - } - } - - public void setPaletteEntry(int index, int r, int g, int b) { - if (index < 0 || index > 255) { - return; - } - - int red = clamp8(r); - int green = clamp8(g); - int blue = clamp8(b); - palette[index] = (red << 16) | (green << 8) | blue; - } - - public int getPaletteEntry(int index) { - if (index < 0 || index > 255) { - return 0; - } - - return palette[index]; - } - - public int[] toRgbBuffer() { - int[] rgb = new int[framebuffer.length]; - for (int i = 0; i < framebuffer.length; i++) { - rgb[i] = palette[framebuffer[i] & 0xFF]; - } - return rgb; - } - - public BufferedImage toBufferedImage() { - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - image.setRGB(0, 0, width, height, toRgbBuffer(), 0, width); - return image; - } - - public void blitToGui(Gui gui, int scale) { - if (gui == null || scale <= 0) { - return; - } - try { - Thread.sleep(1); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - int[] rgb = toRgbBuffer(); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int color = rgb[y * width + x]; - int r = (color >> 16) & 0xFF; - int g = (color >> 8) & 0xFF; - int b = color & 0xFF; - - int baseX = x * scale; - int baseY = y * scale; - - for (int sy = 0; sy < scale; sy++) { - for (int sx = 0; sx < scale; sx++) { - gui.putPixel(baseX + sx, baseY + sy, r, g, b); - } - } - } - } - } - - public void blitToGui(Gui gui) { - blitToGui(gui, 1); - } - - private byte toPaletteIndex(int colorIndex) { - return (byte) (colorIndex & 0xFF); - } - - private int clamp8(int value) { - return Math.max(0, Math.min(255, value)); - } - - private void initializeDefaultPalette() { - int[] ega16 = { - 0x000000, 0x0000AA, 0x00AA00, 0x00AAAA, - 0xAA0000, 0xAA00AA, 0xAA5500, 0xAAAAAA, - 0x555555, 0x5555FF, 0x55FF55, 0x55FFFF, - 0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF - }; - - System.arraycopy(ega16, 0, palette, 0, ega16.length); - - for (int i = 16; i < 256; i++) { - int gray = (int) Math.round(((i - 16) / 239.0) * 255.0); - palette[i] = (gray << 16) | (gray << 8) | gray; - } - } -} diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/machines/Machine.java b/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/machines/Machine.java deleted file mode 100644 index 7113e3a..0000000 --- a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/machines/Machine.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.github._0x4248.nova.BIOS.machines; - -public class Machine { - - public final String id; - public final String biosLabel; - public final int screenScale; - public final boolean supportsSound; - public final boolean supportsKeyboard; - - public Machine(String id, String biosLabel, int screenScale, boolean supportsSound, boolean supportsKeyboard) { - this.id = id; - this.biosLabel = biosLabel; - this.screenScale = screenScale; - this.supportsSound = supportsSound; - this.supportsKeyboard = supportsKeyboard; - } -} diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/machines/StandardMachine.java b/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/machines/StandardMachine.java deleted file mode 100644 index 3b327d1..0000000 --- a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/machines/StandardMachine.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.github._0x4248.nova.BIOS.machines; - -public class StandardMachine extends Machine { - - public StandardMachine() { - super("STANDARD", "NOVA BIOS v0.1", 4, true, true); - } -} diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Core/Gui.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Core/Gui.java index 3a58aff..26543d9 100644 --- a/lab/nova/src/main/java/com/github/_0x4248/nova/Core/Gui.java +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Core/Gui.java @@ -7,6 +7,7 @@ import java.awt.event.*; public class Gui extends JPanel implements KeyListener { + private final JFrame frame; private final BufferedImage framebuffer; private final int framebufferWidth; private final int framebufferHeight; @@ -20,7 +21,7 @@ public class Gui extends JPanel implements KeyListener { keyboard = new Keyboard(); sound = new Sound(); - JFrame frame = new JFrame("NovaEngine"); + frame = new JFrame("NovaEngine"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(width, height); frame.add(this); @@ -68,6 +69,11 @@ public class Gui extends JPanel implements KeyListener { sound.biosBeep(lengthMs, pitchHz); } + public void close() { + frame.setVisible(false); + frame.dispose(); + } + @Override public void keyTyped(KeyEvent e) { } diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/audio/Speaker.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/audio/Speaker.java new file mode 100644 index 0000000..bbeb90a --- /dev/null +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/audio/Speaker.java @@ -0,0 +1,22 @@ +package com.github._0x4248.nova.Machine.audio; + +import com.github._0x4248.nova.Core.Sound; +import com.github._0x4248.nova.Machine.core.Hardware; + +public class Speaker implements Hardware { + + private final Sound sound; + + public Speaker() { + this.sound = new Sound(); + } + + @Override + public String id() { + return "speaker"; + } + + public void beep(int lengthMs, int pitchHz) { + sound.biosBeep(lengthMs, pitchHz); + } +} \ No newline at end of file diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Hardware.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Hardware.java new file mode 100644 index 0000000..7ecfc22 --- /dev/null +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Hardware.java @@ -0,0 +1,12 @@ +package com.github._0x4248.nova.Machine.core; + +public interface Hardware { + + String id(); + + default void onAttach(Machine machine) { + } + + default void onDetach(Machine machine) { + } +} diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Keyboard.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Keyboard.java new file mode 100644 index 0000000..0a1375d --- /dev/null +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Keyboard.java @@ -0,0 +1,23 @@ +package com.github._0x4248.nova.Machine.core; + +public class Keyboard implements Hardware { + + private final Video video; + + public Keyboard(Video video) { + this.video = video; + } + + @Override + public String id() { + return "keyboard"; + } + + public boolean hasKeyPress() { + return video.gpu.hasKeyPress(); + } + + public Integer pollKeyCode() { + return video.gpu.pollKeyCode(); + } +} diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Machine.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Machine.java new file mode 100644 index 0000000..18ea386 --- /dev/null +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Machine.java @@ -0,0 +1,101 @@ +package com.github._0x4248.nova.Machine.core; + +import com.github._0x4248.nova.Machine.audio.Speaker; +import com.github._0x4248.nova.Machine.floppy.Floppy; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class Machine { + + private final Map<String, Hardware> hardware; + + public Machine() { + this.hardware = new LinkedHashMap<>(); + Video video = new Video(); + attach(video); + attach(new Floppy()); + attach(new Speaker()); + attach(new Keyboard(video)); + } + + public static Machine basic() { + return new Machine(); + } + + public static Machine empty() { + return new Machine(false); + } + + private Machine(boolean withDefaults) { + this.hardware = new LinkedHashMap<>(); + if (withDefaults) { + Video video = new Video(); + attach(video); + attach(new Floppy()); + attach(new Speaker()); + attach(new Keyboard(video)); + } + } + + public Machine attach(Hardware device) { + if (device == null) { + throw new IllegalArgumentException("device cannot be null"); + } + + Hardware previous = hardware.put(device.id(), device); + if (previous != null) { + previous.onDetach(this); + } + device.onAttach(this); + System.out.println("Attached hardware: " + device.id()); + return this; + } + + public <T extends Hardware> T get(String id, Class<T> type) { + Hardware device = hardware.get(id); + if (device == null) { + return null; + } + if (!type.isInstance(device)) { + throw new IllegalStateException("Hardware '" + id + "' is not a " + type.getSimpleName()); + } + return type.cast(device); + } + + public boolean has(String id) { + return hardware.containsKey(id); + } + + public Machine detach(String id) { + Hardware removed = hardware.remove(id); + if (removed != null) { + removed.onDetach(this); + } + return this; + } + + private <T extends Hardware> T require(String id, Class<T> type) { + T device = get(id, type); + if (device == null) { + throw new IllegalStateException("Missing required hardware: " + id); + } + return device; + } + + public Video video() { + return require("video", Video.class); + } + + public Floppy floppy() { + return require("floppy", Floppy.class); + } + + public Speaker speaker() { + return require("speaker", Speaker.class); + } + + public Keyboard keyboard() { + return require("keyboard", Keyboard.class); + } +} \ No newline at end of file diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/MachineProgram.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/MachineProgram.java new file mode 100644 index 0000000..7c2fe91 --- /dev/null +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/MachineProgram.java @@ -0,0 +1,6 @@ +package com.github._0x4248.nova.Machine.core; + +public interface MachineProgram { + + void run(Machine machine, String[] args); +} diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Video.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Video.java new file mode 100644 index 0000000..df80962 --- /dev/null +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/core/Video.java @@ -0,0 +1,54 @@ +package com.github._0x4248.nova.Machine.core; + +import com.github._0x4248.nova.Machine.gpu.GPU; +import com.github._0x4248.nova.Machine.gpu.GpuAdapter; +import com.github._0x4248.nova.Machine.gpu.GpuOutputMode; +import com.github._0x4248.nova.Machine.gpu.VideoMode; + +public class Video implements Hardware { + + public final GPU gpu; + public final Modes modes; + + public static final VideoMode ModesCGA = Modes.CGA_GRAPHICS_320x200; + public static final VideoMode ModesVGA = Modes.VGA_GRAPHICS_640x480_16; + + public Video() { + this.modes = new Modes(); + this.gpu = new GPU().init(ModesVGA); + } + + @Override + public String id() { + return "video"; + } + + public static class Modes { + public static final VideoMode CGA_TEXT_40x25 = VideoMode.of( + "CGA_TEXT_40x25", GpuAdapter.CGA, GpuOutputMode.TEXT, 320, 200, 3, 16, false + ); + + public static final VideoMode CGA_GRAPHICS_320x200 = VideoMode.of( + "CGA_GRAPHICS_320x200", GpuAdapter.CGA, GpuOutputMode.VIDEO, 320, 200, 3, 4, false + ); + + public static final VideoMode VGA_TEXT_80x25 = VideoMode.of( + "VGA_TEXT_80x25", GpuAdapter.VGA, GpuOutputMode.TEXT, 640, 400, 2, 16, false + ); + + public static final VideoMode VGA_GRAPHICS_640x480_16 = VideoMode.of( + "VGA_GRAPHICS_640x480_16", GpuAdapter.VGA, GpuOutputMode.VIDEO, 640, 480, 2, 16, false + ); + + public static final VideoMode VGA_GRAPHICS_320x200_256 = VideoMode.of( + "VGA_GRAPHICS_320x200_256", GpuAdapter.VGA, GpuOutputMode.VIDEO, 320, 200, 4, 256, true + ); + + public final VideoMode CGA_TEXT = CGA_TEXT_40x25; + public final VideoMode CGA = CGA_GRAPHICS_320x200; + + public final VideoMode VGA_TEXT = VGA_TEXT_80x25; + public final VideoMode VGA = VGA_GRAPHICS_640x480_16; + public final VideoMode VGA_256 = VGA_GRAPHICS_320x200_256; + } +} diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/floppy/Floppy.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/floppy/Floppy.java new file mode 100644 index 0000000..29be9ed --- /dev/null +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/floppy/Floppy.java @@ -0,0 +1,18 @@ +package com.github._0x4248.nova.Machine.floppy; + +import com.github._0x4248.nova.Machine.core.Hardware; + +import java.nio.file.Files; +import java.nio.file.Path; + +public class Floppy implements Hardware { + + @Override + public String id() { + return "floppy"; + } + + public boolean isInserted(Path mediumPath) { + return mediumPath != null && Files.exists(mediumPath) && Files.isRegularFile(mediumPath); + } +} \ No newline at end of file diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GPU.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GPU.java new file mode 100644 index 0000000..db0def3 --- /dev/null +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GPU.java @@ -0,0 +1,146 @@ +package com.github._0x4248.nova.Machine.gpu; + +import com.github._0x4248.nova.Core.Gui; +import com.github._0x4248.nova.Machine.gpu.drivers.CgaDriver; +import com.github._0x4248.nova.Machine.gpu.drivers.VgaDriver; +import com.github._0x4248.nova.Machine.gpu.drivers.VideoDriver; + +public class GPU { + + private VideoDriver driver; + private Gui screen; + private int scale; + private VideoMode mode; + + public GPU init(VideoMode mode) { + if (mode == null) { + throw new IllegalArgumentException("mode cannot be null"); + } + + closeOutput(); + + this.driver = createDriver(mode); + this.scale = mode.scale(); + this.mode = mode; + this.screen = new Gui(driver.getWidth() * scale, driver.getHeight() * scale); + return this; + } + + public VideoMode getMode() { + ensureInitialized(); + return mode; + } + + public String getModeName() { + return getMode().name(); + } + + public GpuOutputMode getOutputMode() { + ensureInitialized(); + return mode.outputMode(); + } + + public int getColorCount() { + ensureInitialized(); + return mode.colors(); + } + + public int getWidth() { + ensureInitialized(); + return driver.getWidth(); + } + + public int getHeight() { + ensureInitialized(); + return driver.getHeight(); + } + + public int getTextColumns() { + return getWidth() / 8; + } + + public void clear(int colorIndex) { + ensureInitialized(); + driver.clear(colorIndex); + } + + public void drawText(int x, int y, String text, int foregroundColor, int backgroundColor, boolean transparentBackground) { + ensureInitialized(); + driver.drawText(x, y, text, foregroundColor, backgroundColor, transparentBackground); + } + + public void setPixel(int x, int y, int colorIndex) { + ensureInitialized(); + driver.setPixel(x, y, colorIndex); + } + + public void setPaletteEntry(int index, int r, int g, int b) { + ensureInitialized(); + driver.setPaletteEntry(index, r, g, b); + } + + public void present() { + ensureInitialized(); + + int[] rgb = driver.toRgbBuffer(); + int width = driver.getWidth(); + int height = driver.getHeight(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int color = rgb[y * width + x]; + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = color & 0xFF; + + int baseX = x * scale; + int baseY = y * scale; + + for (int sy = 0; sy < scale; sy++) { + for (int sx = 0; sx < scale; sx++) { + screen.putPixel(baseX + sx, baseY + sy, r, g, b); + } + } + } + } + } + + public boolean hasKeyPress() { + ensureInitialized(); + return screen.hasKeyPress(); + } + + public Integer pollKeyCode() { + ensureInitialized(); + return screen.pollKeyCode(); + } + + public void shutdown() { + closeOutput(); + this.driver = null; + this.mode = null; + } + + private VideoDriver createDriver(VideoMode mode) { + if (mode.adapter() == GpuAdapter.CGA) { + return new CgaDriver(mode); + } + if (mode.adapter() == GpuAdapter.VGA) { + return new VgaDriver(mode); + } + throw new IllegalStateException("Unsupported adapter: " + mode.adapter()); + } + + private void ensureInitialized() { + if (driver == null || screen == null || mode == null) { + throw new IllegalStateException("GPU not initialized. Call init(mode) first."); + } + } + + private void closeOutput() { + if (screen != null) { + screen.close(); + screen = null; + } + } +} diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GpuAdapter.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GpuAdapter.java new file mode 100644 index 0000000..652f673 --- /dev/null +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GpuAdapter.java @@ -0,0 +1,6 @@ +package com.github._0x4248.nova.Machine.gpu; + +public enum GpuAdapter { + CGA, + VGA +} diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GpuOutputMode.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GpuOutputMode.java new file mode 100644 index 0000000..4ec9928 --- /dev/null +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/GpuOutputMode.java @@ -0,0 +1,6 @@ +package com.github._0x4248.nova.Machine.gpu; + +public enum GpuOutputMode { + TEXT, + VIDEO +} diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/VideoMode.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/VideoMode.java new file mode 100644 index 0000000..0f2041f --- /dev/null +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/VideoMode.java @@ -0,0 +1,106 @@ +package com.github._0x4248.nova.Machine.gpu; + +public final class VideoMode { + + private final String name; + private final GpuAdapter adapter; + private final GpuOutputMode outputMode; + private final int width; + private final int height; + private final int scale; + private final int colors; + private final boolean paletteChangeSupported; + + private VideoMode( + String name, + GpuAdapter adapter, + GpuOutputMode outputMode, + int width, + int height, + int scale, + int colors, + boolean paletteChangeSupported + ) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("name cannot be blank"); + } + if (adapter == null) { + throw new IllegalArgumentException("adapter cannot be null"); + } + if (outputMode == null) { + throw new IllegalArgumentException("outputMode cannot be null"); + } + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("width and height must be positive"); + } + if (scale <= 0) { + throw new IllegalArgumentException("scale must be positive"); + } + if (colors <= 0 || colors > 256) { + throw new IllegalArgumentException("colors must be between 1 and 256"); + } + + this.name = name; + this.adapter = adapter; + this.outputMode = outputMode; + this.width = width; + this.height = height; + this.scale = scale; + this.colors = colors; + this.paletteChangeSupported = paletteChangeSupported; + } + + public static VideoMode of( + String name, + GpuAdapter adapter, + GpuOutputMode outputMode, + int width, + int height, + int scale, + int colors, + boolean paletteChangeSupported + ) { + return new VideoMode(name, adapter, outputMode, width, height, scale, colors, paletteChangeSupported); + } + + public String name() { + return name; + } + + public GpuAdapter adapter() { + return adapter; + } + + public GpuOutputMode outputMode() { + return outputMode; + } + + public int width() { + return width; + } + + public int height() { + return height; + } + + public int scale() { + return scale; + } + + public int colors() { + return colors; + } + + public boolean supportsPaletteChanges() { + return paletteChangeSupported; + } + + public boolean supportsPixels() { + return outputMode == GpuOutputMode.VIDEO; + } + + @Override + public String toString() { + return adapter + " " + outputMode + " " + width + "x" + height + " (" + colors + " colors)"; + } +} \ No newline at end of file diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/CgaDriver.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/CgaDriver.java new file mode 100644 index 0000000..9b8cf0f --- /dev/null +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/CgaDriver.java @@ -0,0 +1,138 @@ +package com.github._0x4248.nova.Machine.gpu.drivers; + +import com.github._0x4248.nova.Machine.gpu.GpuAdapter; +import com.github._0x4248.nova.Machine.gpu.GpuOutputMode; +import com.github._0x4248.nova.Machine.gpu.VideoMode; +import com.github._0x4248.nova.Machine.gpu.fonts.VgaFonts; + +import java.util.Arrays; + +public class CgaDriver implements VideoDriver { + + private final VideoMode mode; + private final byte[] framebuffer; + private final int[] palette; + + public CgaDriver(VideoMode mode) { + if (mode.adapter() != GpuAdapter.CGA) { + throw new IllegalArgumentException("CgaDriver requires a CGA mode"); + } + this.mode = mode; + this.framebuffer = new byte[mode.width() * mode.height()]; + this.palette = new int[16]; + initializePalette(); + } + + @Override + public int getWidth() { + return mode.width(); + } + + @Override + public int getHeight() { + return mode.height(); + } + + @Override + public int getColorCount() { + return mode.colors(); + } + + @Override + public GpuOutputMode getOutputMode() { + return mode.outputMode(); + } + + @Override + public void clear(int colorIndex) { + Arrays.fill(framebuffer, (byte) sanitizeColor(colorIndex)); + } + + @Override + public void drawText(int x, int y, String text, int foregroundColor, int backgroundColor, boolean transparentBackground) { + int cursorX = x; + int cursorY = y; + + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + if (character == '\n') { + cursorX = x; + cursorY += 8; + continue; + } + + drawChar(cursorX, cursorY, character, foregroundColor, backgroundColor, transparentBackground); + cursorX += 8; + } + } + + @Override + public void setPixel(int x, int y, int colorIndex) { + if (mode.outputMode() != GpuOutputMode.VIDEO) { + throw new IllegalStateException("Current mode is TEXT. Switch to a VIDEO mode to use pixels."); + } + if (x < 0 || y < 0 || x >= mode.width() || y >= mode.height()) { + return; + } + framebuffer[y * mode.width() + x] = (byte) sanitizeColor(colorIndex); + } + + @Override + public void setPaletteEntry(int index, int r, int g, int b) { + throw new IllegalStateException("CGA palette is fixed in this driver"); + } + + @Override + public int[] toRgbBuffer() { + int[] rgb = new int[framebuffer.length]; + for (int i = 0; i < framebuffer.length; i++) { + rgb[i] = palette[framebuffer[i] & 0x0F]; + } + return rgb; + } + + private void drawChar(int x, int y, char character, int foregroundColor, int backgroundColor, boolean transparentBackground) { + int glyphIndex = character & 0x7F; + if (glyphIndex >= VgaFonts.LATIN8x8.length) { + glyphIndex = '?'; + } + + byte[] glyph = VgaFonts.LATIN8x8[glyphIndex]; + int foreground = sanitizeColor(foregroundColor); + int background = sanitizeColor(backgroundColor); + + for (int row = 0; row < 8; row++) { + int rowBits = glyph[row] & 0xFF; + for (int col = 0; col < 8; col++) { + boolean on = ((rowBits >> col) & 1) == 1; + if (on) { + writePixel(x + col, y + row, foreground); + } else if (!transparentBackground) { + writePixel(x + col, y + row, background); + } + } + } + } + + private void writePixel(int x, int y, int colorIndex) { + if (x < 0 || y < 0 || x >= mode.width() || y >= mode.height()) { + return; + } + framebuffer[y * mode.width() + x] = (byte) sanitizeColor(colorIndex); + } + + private int sanitizeColor(int colorIndex) { + return Math.floorMod(colorIndex, mode.colors()); + } + + private void initializePalette() { + int[] cga16 = { + 0x000000, 0x0000AA, 0x00AA00, 0x00AAAA, + 0xAA0000, 0xAA00AA, 0xAA5500, 0xAAAAAA, + 0x555555, 0x5555FF, 0x55FF55, 0x55FFFF, + 0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF + }; + + System.arraycopy(cga16, 0, palette, 0, cga16.length); + } +} diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/VgaDriver.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/VgaDriver.java new file mode 100644 index 0000000..effc264 --- /dev/null +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/VgaDriver.java @@ -0,0 +1,156 @@ +package com.github._0x4248.nova.Machine.gpu.drivers; + +import com.github._0x4248.nova.Machine.gpu.GpuAdapter; +import com.github._0x4248.nova.Machine.gpu.GpuOutputMode; +import com.github._0x4248.nova.Machine.gpu.VideoMode; +import com.github._0x4248.nova.Machine.gpu.fonts.VgaFonts; + +import java.util.Arrays; + +public class VgaDriver implements VideoDriver { + + private final VideoMode mode; + private final byte[] framebuffer; + private final int[] palette; + + public VgaDriver(VideoMode mode) { + if (mode.adapter() != GpuAdapter.VGA) { + throw new IllegalArgumentException("VgaDriver requires a VGA mode"); + } + this.mode = mode; + this.framebuffer = new byte[mode.width() * mode.height()]; + this.palette = new int[256]; + initializePalette(); + } + + @Override + public int getWidth() { + return mode.width(); + } + + @Override + public int getHeight() { + return mode.height(); + } + + @Override + public int getColorCount() { + return mode.colors(); + } + + @Override + public GpuOutputMode getOutputMode() { + return mode.outputMode(); + } + + @Override + public void clear(int colorIndex) { + Arrays.fill(framebuffer, (byte) sanitizeColor(colorIndex)); + } + + @Override + public void drawText(int x, int y, String text, int foregroundColor, int backgroundColor, boolean transparentBackground) { + int cursorX = x; + int cursorY = y; + + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + if (character == '\n') { + cursorX = x; + cursorY += 8; + continue; + } + + drawChar(cursorX, cursorY, character, foregroundColor, backgroundColor, transparentBackground); + cursorX += 8; + } + } + + @Override + public void setPixel(int x, int y, int colorIndex) { + if (mode.outputMode() != GpuOutputMode.VIDEO) { + throw new IllegalStateException("Current mode is TEXT. Switch to a VIDEO mode to use pixels."); + } + if (x < 0 || y < 0 || x >= mode.width() || y >= mode.height()) { + return; + } + framebuffer[y * mode.width() + x] = (byte) sanitizeColor(colorIndex); + } + + @Override + public void setPaletteEntry(int index, int r, int g, int b) { + if (!mode.supportsPaletteChanges()) { + throw new IllegalStateException("Palette changes are not supported in mode " + mode.name()); + } + if (index < 0 || index >= mode.colors()) { + return; + } + int red = clamp8(r); + int green = clamp8(g); + int blue = clamp8(b); + palette[index] = (red << 16) | (green << 8) | blue; + } + + @Override + public int[] toRgbBuffer() { + int[] rgb = new int[framebuffer.length]; + for (int i = 0; i < framebuffer.length; i++) { + rgb[i] = palette[framebuffer[i] & 0xFF]; + } + return rgb; + } + + private void drawChar(int x, int y, char character, int foregroundColor, int backgroundColor, boolean transparentBackground) { + int glyphIndex = character & 0x7F; + if (glyphIndex >= VgaFonts.LATIN8x8.length) { + glyphIndex = '?'; + } + + byte[] glyph = VgaFonts.LATIN8x8[glyphIndex]; + int foreground = sanitizeColor(foregroundColor); + int background = sanitizeColor(backgroundColor); + + for (int row = 0; row < 8; row++) { + int rowBits = glyph[row] & 0xFF; + for (int col = 0; col < 8; col++) { + boolean on = ((rowBits >> col) & 1) == 1; + if (on) { + writePixel(x + col, y + row, foreground); + } else if (!transparentBackground) { + writePixel(x + col, y + row, background); + } + } + } + } + + private void writePixel(int x, int y, int colorIndex) { + if (x < 0 || y < 0 || x >= mode.width() || y >= mode.height()) { + return; + } + framebuffer[y * mode.width() + x] = (byte) sanitizeColor(colorIndex); + } + + private int sanitizeColor(int colorIndex) { + return Math.floorMod(colorIndex, mode.colors()); + } + + private int clamp8(int value) { + return Math.max(0, Math.min(255, value)); + } + + private void initializePalette() { + int[] ega16 = { + 0x000000, 0x0000AA, 0x00AA00, 0x00AAAA, + 0xAA0000, 0xAA00AA, 0xAA5500, 0xAAAAAA, + 0x555555, 0x5555FF, 0x55FF55, 0x55FFFF, + 0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF + }; + + System.arraycopy(ega16, 0, palette, 0, ega16.length); + + for (int i = 16; i < 256; i++) { + int gray = (int) Math.round(((i - 16) / 239.0) * 255.0); + palette[i] = (gray << 16) | (gray << 8) | gray; + } + } +} diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/VideoDriver.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/VideoDriver.java new file mode 100644 index 0000000..4983187 --- /dev/null +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/drivers/VideoDriver.java @@ -0,0 +1,24 @@ +package com.github._0x4248.nova.Machine.gpu.drivers; + +import com.github._0x4248.nova.Machine.gpu.GpuOutputMode; + +public interface VideoDriver { + + int getWidth(); + + int getHeight(); + + int getColorCount(); + + GpuOutputMode getOutputMode(); + + void clear(int colorIndex); + + void drawText(int x, int y, String text, int foregroundColor, int backgroundColor, boolean transparentBackground); + + void setPixel(int x, int y, int colorIndex); + + void setPaletteEntry(int index, int r, int g, int b); + + int[] toRgbBuffer(); +} diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/VGA/VGAFonts.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/fonts/VgaFonts.java similarity index 99% rename from lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/VGA/VGAFonts.java rename to lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/fonts/VgaFonts.java index de366a8..ad4890c 100644 --- a/lab/nova/src/main/java/com/github/_0x4248/nova/BIOS/VGA/VGAFonts.java +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Machine/gpu/fonts/VgaFonts.java @@ -1,6 +1,6 @@ -package com.github._0x4248.nova.BIOS.VGA; +package com.github._0x4248.nova.Machine.gpu.fonts; -public class VGAFonts { +public class VgaFonts { public static final byte[][] LATIN8x8 = { { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0000 (nul) { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0001 diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova/Main.java b/lab/nova/src/main/java/com/github/_0x4248/nova/Main.java index 56f99e6..cbc403b 100644 --- a/lab/nova/src/main/java/com/github/_0x4248/nova/Main.java +++ b/lab/nova/src/main/java/com/github/_0x4248/nova/Main.java @@ -1,27 +1,46 @@ package com.github._0x4248.nova; -import com.github._0x4248.nova.BIOS.BIOS; -import com.github._0x4248.nova.BIOS.machines.StandardMachine; +import com.github._0x4248.nova.Machine.core.Machine; +import com.github._0x4248.nova.Machine.core.MachineProgram; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; public class Main { - private static final String DEFAULT_INTERNAL_APP = "com.github._0x4248.nova_examples.ExampleXYZ"; + private static final String DEFAULT_INTERNAL_APP = "com.github._0x4248.nova_examples.NovaBasic"; public static void main(String[] args) { - BIOS bios = new BIOS(new StandardMachine()); - Path applicationJar = Paths.get("Application.jar"); - - boolean booted; - if (java.nio.file.Files.exists(applicationJar)) { - booted = bios.bootApplication(applicationJar, args); - } else { - booted = bios.bootInternalApplication(DEFAULT_INTERNAL_APP, args); - } + String targetProgram = args.length > 0 ? args[0] : DEFAULT_INTERNAL_APP; + String[] programArgs = args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]; - if (!booted) { + Machine machine = Machine.basic(); + if (!runProgram(targetProgram, machine, programArgs)) { System.exit(1); } } + + private static boolean runProgram(String className, Machine machine, String[] args) { + try { + Class<?> programClass = Class.forName(className); + + if (MachineProgram.class.isAssignableFrom(programClass)) { + MachineProgram program = (MachineProgram) programClass.getDeclaredConstructor().newInstance(); + program.run(machine, args == null ? new String[0] : args); + return true; + } + + Method mainMethod = programClass.getMethod("main", String[].class); + int modifiers = mainMethod.getModifiers(); + if (!Modifier.isPublic(modifiers) || !Modifier.isStatic(modifiers)) { + throw new IllegalStateException("main(String[]) must be public static"); + } + + mainMethod.invoke(null, (Object) (args == null ? new String[0] : args)); + return true; + } catch (Exception e) { + System.err.println("Failed to run program '" + className + "': " + e.getMessage()); + return false; + } + } } \ No newline at end of file diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova_examples/ExampleXYZ.java b/lab/nova/src/main/java/com/github/_0x4248/nova_examples/ExampleXYZ.java index b15add9..26ba9e9 100644 --- a/lab/nova/src/main/java/com/github/_0x4248/nova_examples/ExampleXYZ.java +++ b/lab/nova/src/main/java/com/github/_0x4248/nova_examples/ExampleXYZ.java @@ -1,49 +1,71 @@ package com.github._0x4248.nova_examples; -import com.github._0x4248.nova.BIOS.BIOS; -import com.github._0x4248.nova.BIOS.BiosRuntime; -import com.github._0x4248.nova.BIOS.machines.StandardMachine; +import com.github._0x4248.nova.Machine.core.Keyboard; +import com.github._0x4248.nova.Machine.core.Machine; +import com.github._0x4248.nova.Machine.core.MachineProgram; +import com.github._0x4248.nova.Machine.core.Video; +import com.github._0x4248.nova.Machine.gpu.GPU; -public class ExampleXYZ { - public static final int BOOTFLAG = 1; +import java.awt.event.KeyEvent; + +public class ExampleXYZ implements MachineProgram { public static void main(String[] args) { - System.out.println("Launching Example XYZ..."); - BiosRuntime bios = BIOS.getRuntime(); - if (bios == null) { - bios = new BiosRuntime(new StandardMachine()); - } + new ExampleXYZ().run(Machine.basic(), args); + } + + @Override + public void run(Machine machine, String[] args) { + GPU gpu = machine.video().gpu; + Keyboard keyboard = machine.keyboard(); - bios.clear(1); - bios.drawText(24, 32, "HELLO WORLD", 15, 1, false); - bios.drawText(24, 48, "NOVA EXAMPLE XYZ", 14, 1, false); + gpu.init(Video.Modes.CGA_TEXT_40x25); + gpu.clear(0); + gpu.drawText(8, 8, "NOVA TEXT MODE", 15, 0, false); + gpu.drawText(8, 20, "Press ENTER for VGA graphics", 14, 0, false); + gpu.drawText(8, 32, "ESC exits", 10, 0, false); + gpu.present(); - if (!bios.getMachine().supportsSound) { - bios.drawText(24, 64, "NO SOUND DEVICE", 12, 1, false); + while (true) { + Integer keyCode = keyboard.pollKeyCode(); + if (keyCode == null) { + sleep(10); + continue; + } + if (keyCode == KeyEvent.VK_ESCAPE) { + break; + } + if (keyCode == KeyEvent.VK_ENTER) { + break; + } } - bios.present(); - try{ - Thread.sleep(2500); - } catch (InterruptedException e) { - e.printStackTrace(); + gpu.init(Video.Modes.VGA_GRAPHICS_640x480_16); + gpu.clear(1); + gpu.drawText(8, 8, "VGA VIDEO MODE", 15, 1, false); + + for (int x = 0; x < gpu.getWidth(); x++) { + int y = (int) (gpu.getHeight() * 0.5 + Math.sin(x / 20.0) * 50); + gpu.setPixel(x, y, 14); } + gpu.present(); + while (true) { - bios.clear(0); - bios.beep(200, 440); - bios.present(); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - bios.drawText(0, 0, "This is just an example", 15, 0, false); - bios.present(); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } + Integer keyCode = keyboard.pollKeyCode(); + if (keyCode != null && keyCode == KeyEvent.VK_ESCAPE) { + break; + } + sleep(10); + } + + gpu.shutdown(); + } + + private void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } } diff --git a/lab/nova/src/main/java/com/github/_0x4248/nova_examples/NovaBasic.java b/lab/nova/src/main/java/com/github/_0x4248/nova_examples/NovaBasic.java new file mode 100644 index 0000000..249f6f6 --- /dev/null +++ b/lab/nova/src/main/java/com/github/_0x4248/nova_examples/NovaBasic.java @@ -0,0 +1,682 @@ +package com.github._0x4248.nova_examples; + +import com.github._0x4248.nova.Machine.core.Machine; +import com.github._0x4248.nova.Machine.core.MachineProgram; +import com.github._0x4248.nova.Machine.core.Keyboard; +import com.github._0x4248.nova.Machine.core.Video; +import com.github._0x4248.nova.Machine.gpu.GPU; + +import java.awt.event.KeyEvent; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.TreeMap; + +public class NovaBasic implements MachineProgram { + + private static final int VIDEO_COLUMNS = 40; + private static final int VIDEO_ROWS = 25; + + private final TreeMap<Integer, String> program = new TreeMap<>(); + private final Map<String, Integer> variables = new HashMap<>(); + private final List<String> screenLines = new ArrayList<>(); + private GPU gpu; + private Keyboard keyboard; + private String currentInput = ""; + + public static void main(String[] args) { + new NovaBasic().run(Machine.basic(), args); + } + + @Override + public void run(Machine machine, String[] args) { + initVideoOut(machine); + initKeyboardIn(machine); + + outLine("NOVA BASIC V0.1"); + outLine("READY."); + outLine("Type HELP for commands."); + + if (gpu != null && keyboard != null) { + runKeyboardLoop(); + if (gpu != null) { + gpu.shutdown(); + } + return; + } + + try (Scanner scanner = new Scanner(System.in)) { + while (true) { + System.out.print("> "); + if (!scanner.hasNextLine()) { + break; + } + + String line = scanner.nextLine().trim(); + appendScreenLine("> " + line); + if (line.isEmpty()) { + continue; + } + + if (processInputLine(line)) { + break; + } + } + } + + if (gpu != null) { + gpu.shutdown(); + } + } + + private void runKeyboardLoop() { + while (true) { + String line = readLineFromKeyboard(); + if (line == null) { + continue; + } + + String trimmed = line.trim(); + if (trimmed.isEmpty()) { + continue; + } + + if (processInputLine(trimmed)) { + break; + } + } + } + + private boolean processInputLine(String line) { + if (startsWithLineNumber(line)) { + saveProgramLine(line); + return false; + } + + return handleDirectCommand(line); + } + + private boolean handleDirectCommand(String line) { + String upper = line.toUpperCase(); + + if (upper.equals("RUN")) { + runProgram(); + return false; + } + if (upper.equals("LIST")) { + listProgram(); + return false; + } + if (upper.equals("NEW")) { + program.clear(); + variables.clear(); + outLine("OK"); + return false; + } + if (upper.equals("HELP")) { + printHelp(); + return false; + } + if (upper.equals("VARS")) { + printVars(); + return false; + } + if (upper.equals("EXIT") || upper.equals("QUIT") || upper.equals("BYE")) { + outLine("BYE"); + return true; + } + + try { + executeStatement(line, null, null); + } catch (BasicRuntimeException e) { + outLine("? " + e.getMessage()); + } + return false; + } + + private boolean startsWithLineNumber(String line) { + int idx = 0; + while (idx < line.length() && Character.isDigit(line.charAt(idx))) { + idx++; + } + return idx > 0 && (idx == line.length() || Character.isWhitespace(line.charAt(idx))); + } + + private void saveProgramLine(String line) { + int idx = 0; + while (idx < line.length() && Character.isDigit(line.charAt(idx))) { + idx++; + } + + int lineNumber = Integer.parseInt(line.substring(0, idx)); + String statement = line.substring(idx).trim(); + + if (statement.isEmpty()) { + program.remove(lineNumber); + } else { + program.put(lineNumber, statement); + } + } + + private void listProgram() { + if (program.isEmpty()) { + outLine("(empty)"); + return; + } + for (Map.Entry<Integer, String> entry : program.entrySet()) { + outLine(entry.getKey() + " " + entry.getValue()); + } + } + + private void printHelp() { + outLine("DIRECT: RUN, LIST, NEW, VARS, HELP, QUIT"); + outLine("PROGRAM: PRINT, LET, GOTO, IF ... THEN <line>, END, REM"); + outLine("EXAMPLE:"); + outLine("10 LET X = 1"); + outLine("20 PRINT X"); + outLine("30 LET X = X + 1"); + outLine("40 IF X < 5 THEN 20"); + outLine("50 END"); + } + + private void printVars() { + if (variables.isEmpty()) { + outLine("(no vars)"); + return; + } + + for (Map.Entry<String, Integer> entry : variables.entrySet()) { + outLine(entry.getKey() + " = " + entry.getValue()); + } + } + + private void runProgram() { + if (program.isEmpty()) { + outLine("NO PROGRAM"); + return; + } + + List<Integer> lineNumbers = new ArrayList<>(program.keySet()); + int pc = 0; + int stepCounter = 0; + final int maxSteps = 100_000; + + try { + while (pc >= 0 && pc < lineNumbers.size()) { + if (++stepCounter > maxSteps) { + throw new BasicRuntimeException("TOO MANY STEPS (possible infinite loop)"); + } + + int currentLine = lineNumbers.get(pc); + String statement = program.get(currentLine); + + ExecutionResult result = executeStatement(statement, lineNumbers, pc); + if (result.endProgram) { + break; + } + if (result.nextPc != null) { + pc = result.nextPc; + } else { + pc++; + } + } + + outLine("READY."); + } catch (BasicRuntimeException e) { + outLine("? " + e.getMessage()); + } + } + + private ExecutionResult executeStatement(String statement, List<Integer> lineNumbers, Integer currentPc) { + String trimmed = statement.trim(); + if (trimmed.isEmpty()) { + return ExecutionResult.continueNext(); + } + + String upper = trimmed.toUpperCase(); + + if (upper.startsWith("REM") || upper.startsWith("'")) { + return ExecutionResult.continueNext(); + } + + if (upper.startsWith("PRINT")) { + String arg = trimmed.length() > 5 ? trimmed.substring(5).trim() : ""; + doPrint(arg); + return ExecutionResult.continueNext(); + } + + if (upper.startsWith("LET")) { + String assign = trimmed.substring(3).trim(); + doLet(assign); + return ExecutionResult.continueNext(); + } + + if (upper.startsWith("IF")) { + return doIf(trimmed, lineNumbers); + } + + if (upper.startsWith("GOTO")) { + String arg = trimmed.substring(4).trim(); + int target = parseLineNumber(arg); + return ExecutionResult.jump(findLineIndex(lineNumbers, target)); + } + + if (upper.equals("END")) { + return ExecutionResult.end(); + } + + if (upper.equals("RUN")) { + runProgram(); + return ExecutionResult.end(); + } + + if (trimmed.matches("^[A-Za-z][A-Za-z0-9_]*\\s*=.*$")) { + doLet(trimmed); + return ExecutionResult.continueNext(); + } + + throw new BasicRuntimeException("SYNTAX ERROR: " + statement); + } + + private void doPrint(String arg) { + if (arg.isEmpty()) { + outLine(""); + return; + } + + if ((arg.startsWith("\"") && arg.endsWith("\"")) || (arg.startsWith("'") && arg.endsWith("'"))) { + outLine(arg.substring(1, arg.length() - 1)); + return; + } + + int value = evalExpr(arg); + outLine(String.valueOf(value)); + } + + private void initVideoOut(Machine machine) { + try { + gpu = machine.video().gpu; + gpu.init(Video.Modes.CGA_TEXT_40x25); + redrawVideo(); + } catch (Exception ignored) { + gpu = null; + } + } + + private void initKeyboardIn(Machine machine) { + try { + keyboard = machine.keyboard(); + } catch (Exception ignored) { + keyboard = null; + } + } + + private void outLine(String text) { + System.out.println(text); + appendScreenLine(text); + } + + private void appendScreenLine(String text) { + String value = text == null ? "" : text; + for (String part : wrapLine(value)) { + screenLines.add(part); + } + + while (screenLines.size() > VIDEO_ROWS - 2) { + screenLines.remove(0); + } + + redrawVideo(); + } + + private List<String> wrapLine(String text) { + List<String> wrapped = new ArrayList<>(); + if (text.isEmpty()) { + wrapped.add(""); + return wrapped; + } + + int start = 0; + while (start < text.length()) { + int end = Math.min(start + VIDEO_COLUMNS, text.length()); + wrapped.add(text.substring(start, end)); + start = end; + } + return wrapped; + } + + private void redrawVideo() { + if (gpu == null) { + return; + } + + gpu.clear(0); + gpu.drawText(0, 0, fitLine("NOVA BASIC"), 15, 0, false); + + int row = 1; + for (String line : screenLines) { + if (row >= VIDEO_ROWS - 1) { + break; + } + gpu.drawText(0, row * 8, fitLine(line), 15, 0, false); + row++; + } + + String prompt = "> " + currentInput + "_"; + gpu.drawText(0, (VIDEO_ROWS - 1) * 8, fitLine(prompt), 14, 0, false); + + gpu.present(); + } + + private String readLineFromKeyboard() { + StringBuilder builder = new StringBuilder(); + currentInput = ""; + redrawVideo(); + + while (true) { + Integer keyCode = keyboard.pollKeyCode(); + if (keyCode == null) { + sleep(10); + continue; + } + + if (keyCode == KeyEvent.VK_ENTER) { + String line = builder.toString(); + appendScreenLine("> " + line); + System.out.println("> " + line); + currentInput = ""; + redrawVideo(); + return line; + } + + if (keyCode == KeyEvent.VK_BACK_SPACE) { + if (builder.length() > 0) { + builder.deleteCharAt(builder.length() - 1); + currentInput = builder.toString(); + redrawVideo(); + } + continue; + } + + if (keyCode == KeyEvent.VK_ESCAPE) { + builder.setLength(0); + builder.append("QUIT"); + currentInput = builder.toString(); + redrawVideo(); + continue; + } + + Character mapped = mapKeyCodeToChar(keyCode); + if (mapped != null) { + builder.append(mapped); + currentInput = builder.toString(); + redrawVideo(); + } + } + } + + private Character mapKeyCodeToChar(int keyCode) { + if (keyCode >= KeyEvent.VK_A && keyCode <= KeyEvent.VK_Z) { + return (char) ('A' + (keyCode - KeyEvent.VK_A)); + } + if (keyCode >= KeyEvent.VK_0 && keyCode <= KeyEvent.VK_9) { + return (char) ('0' + (keyCode - KeyEvent.VK_0)); + } + if (keyCode >= KeyEvent.VK_NUMPAD0 && keyCode <= KeyEvent.VK_NUMPAD9) { + return (char) ('0' + (keyCode - KeyEvent.VK_NUMPAD0)); + } + + return switch (keyCode) { + case KeyEvent.VK_SPACE -> ' '; + case KeyEvent.VK_PERIOD, KeyEvent.VK_DECIMAL -> '.'; + case KeyEvent.VK_COMMA -> ','; + case KeyEvent.VK_MINUS, KeyEvent.VK_SUBTRACT -> '-'; + case KeyEvent.VK_EQUALS -> '='; + case KeyEvent.VK_PLUS, KeyEvent.VK_ADD -> '+'; + case KeyEvent.VK_SLASH, KeyEvent.VK_DIVIDE -> '/'; + case KeyEvent.VK_ASTERISK, KeyEvent.VK_MULTIPLY -> '*'; + case KeyEvent.VK_SEMICOLON -> ';'; + case KeyEvent.VK_COLON -> ':'; + case KeyEvent.VK_QUOTE -> '\''; + case KeyEvent.VK_OPEN_BRACKET -> '['; + case KeyEvent.VK_CLOSE_BRACKET -> ']'; + case KeyEvent.VK_BACK_SLASH -> '\\'; + default -> null; + }; + } + + private void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private String fitLine(String text) { + String value = text == null ? "" : text; + if (value.length() > VIDEO_COLUMNS) { + return value.substring(0, VIDEO_COLUMNS); + } + return value + " ".repeat(VIDEO_COLUMNS - value.length()); + } + + private void doLet(String assign) { + int eq = assign.indexOf('='); + if (eq <= 0) { + throw new BasicRuntimeException("LET EXPECTS: NAME = EXPR"); + } + + String name = assign.substring(0, eq).trim().toUpperCase(); + if (!name.matches("[A-Z][A-Z0-9_]*")) { + throw new BasicRuntimeException("BAD VARIABLE NAME: " + name); + } + + String expr = assign.substring(eq + 1).trim(); + int value = evalExpr(expr); + variables.put(name, value); + } + + private ExecutionResult doIf(String statement, List<Integer> lineNumbers) { + String body = statement.substring(2).trim(); + int thenPos = body.toUpperCase().indexOf("THEN"); + if (thenPos < 0) { + throw new BasicRuntimeException("IF EXPECTS THEN"); + } + + String cond = body.substring(0, thenPos).trim(); + String thenTarget = body.substring(thenPos + 4).trim(); + + if (evaluateCondition(cond)) { + int target = parseLineNumber(thenTarget); + return ExecutionResult.jump(findLineIndex(lineNumbers, target)); + } + return ExecutionResult.continueNext(); + } + + private boolean evaluateCondition(String cond) { + String[] ops = {"<=", ">=", "<>", "=", "<", ">"}; + + for (String op : ops) { + int idx = cond.indexOf(op); + if (idx > 0) { + int left = evalExpr(cond.substring(0, idx).trim()); + int right = evalExpr(cond.substring(idx + op.length()).trim()); + + return switch (op) { + case "=" -> left == right; + case "<>" -> left != right; + case "<" -> left < right; + case ">" -> left > right; + case "<=" -> left <= right; + case ">=" -> left >= right; + default -> false; + }; + } + } + + throw new BasicRuntimeException("BAD IF CONDITION: " + cond); + } + + private int parseLineNumber(String text) { + try { + return Integer.parseInt(text.trim()); + } catch (NumberFormatException e) { + throw new BasicRuntimeException("BAD LINE NUMBER: " + text); + } + } + + private int findLineIndex(List<Integer> lineNumbers, int line) { + if (lineNumbers == null) { + throw new BasicRuntimeException("GOTO/IF THEN needs a running program"); + } + + int idx = lineNumbers.indexOf(line); + if (idx < 0) { + throw new BasicRuntimeException("UNDEFINED LINE " + line); + } + return idx; + } + + private int evalExpr(String expr) { + return new ExpressionParser(expr).parse(); + } + + private static final class ExecutionResult { + private final Integer nextPc; + private final boolean endProgram; + + private ExecutionResult(Integer nextPc, boolean endProgram) { + this.nextPc = nextPc; + this.endProgram = endProgram; + } + + private static ExecutionResult continueNext() { + return new ExecutionResult(null, false); + } + + private static ExecutionResult jump(int nextPc) { + return new ExecutionResult(nextPc, false); + } + + private static ExecutionResult end() { + return new ExecutionResult(null, true); + } + } + + private static final class BasicRuntimeException extends RuntimeException { + private BasicRuntimeException(String message) { + super(message); + } + } + + private final class ExpressionParser { + private final String input; + private int pos; + + private ExpressionParser(String input) { + this.input = input; + } + + private int parse() { + int value = parseExpression(); + skipSpaces(); + if (pos != input.length()) { + throw new BasicRuntimeException("BAD EXPR: " + input); + } + return value; + } + + private int parseExpression() { + int value = parseTerm(); + while (true) { + skipSpaces(); + if (match('+')) { + value += parseTerm(); + } else if (match('-')) { + value -= parseTerm(); + } else { + return value; + } + } + } + + private int parseTerm() { + int value = parseFactor(); + while (true) { + skipSpaces(); + if (match('*')) { + value *= parseFactor(); + } else if (match('/')) { + int divisor = parseFactor(); + if (divisor == 0) { + throw new BasicRuntimeException("DIV BY ZERO"); + } + value /= divisor; + } else { + return value; + } + } + } + + private int parseFactor() { + skipSpaces(); + + if (match('(')) { + int value = parseExpression(); + skipSpaces(); + if (!match(')')) { + throw new BasicRuntimeException("MISSING )"); + } + return value; + } + + if (match('-')) { + return -parseFactor(); + } + + if (pos < input.length() && Character.isDigit(input.charAt(pos))) { + return parseNumber(); + } + + if (pos < input.length() && Character.isLetter(input.charAt(pos))) { + String name = parseIdentifier().toUpperCase(); + return variables.getOrDefault(name, 0); + } + + throw new BasicRuntimeException("BAD EXPR: " + input); + } + + private int parseNumber() { + int start = pos; + while (pos < input.length() && Character.isDigit(input.charAt(pos))) { + pos++; + } + return Integer.parseInt(input.substring(start, pos)); + } + + private String parseIdentifier() { + int start = pos; + while (pos < input.length() && (Character.isLetterOrDigit(input.charAt(pos)) || input.charAt(pos) == '_')) { + pos++; + } + return input.substring(start, pos); + } + + private void skipSpaces() { + while (pos < input.length() && Character.isWhitespace(input.charAt(pos))) { + pos++; + } + } + + private boolean match(char expected) { + if (pos < input.length() && input.charAt(pos) == expected) { + pos++; + return true; + } + return false; + } + } +}[COMMIT END](C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.