diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 65a82ee..bd83ae2 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -31,6 +31,7 @@ else() # -------------------------------------------------------------------------- message(STATUS "[Nova64] Building as Desktop Shared Library...") + add_compile_definitions(NOVA64_SHUTDOWN_AVAILABLE) # Enable shutdown functionality for desktop builds # -------------------------------------------------------------------------- # FreeRTOS Configuration diff --git a/core/include/nova64_core.h b/core/include/nova64_core.h index 771a053..7eea708 100644 --- a/core/include/nova64_core.h +++ b/core/include/nova64_core.h @@ -1,9 +1,10 @@ #ifndef NOVA64_CORE_H #define NOVA64_CORE_H +#include #include #include -#include + // Cross-platform DLL export/import visibility macros #if defined(_WIN32) || defined(__CYGWIN__) @@ -42,16 +43,27 @@ typedef void (*nova64_audio_output_cb_t)(const int16_t *samples, uint32_t count); /** - * @brief Callback to read sectors from the SD card. - * @param sector_idx The zero-based sector index to read. - * @param destination_buffer Pointer to memory where sector data should be - * copied. - * @param sector_count Number of 512-byte sectors to read. - * @return True on success, false on read error. + * @brief Callback to read data from the SD card. + * @param slot_idx Zero-based cartridge slot index. + * @param file_path Null-terminated path to the file to read. + * @param destination_buffer Pointer to memory where file data should be copied. + * @param destination_length Maximum number of bytes available in the + * destination buffer. + * @return Number of bytes read on success, negative value on read error. */ -typedef bool (*nova64_sd_read_sector_cb_t)(uint32_t sector_idx, - uint8_t *destination_buffer, - uint32_t sector_count); +typedef int32_t (*nova64_sd_read_file_cb_t)(uint32_t slot_idx, + const char *file_path, + uint8_t *destination_buffer, + uint32_t destination_length); + +/** + * @brief Callback to retrieve the size of a file on the SD card. + * @param slot_idx Zero-based cartridge slot index. + * @param file_path Null-terminated path to the file. + * @return File size in bytes on success, negative value on error. + */ +typedef int32_t (*nova64_sd_get_file_size_cb_t)(uint32_t slot_idx, + const char *file_path); /** * @brief Callback triggered when a cartridge state change occurs (e.g. @@ -60,7 +72,7 @@ typedef bool (*nova64_sd_read_sector_cb_t)(uint32_t sector_idx, * @param is_inserted True if a cartridge is detected, false if ejected. */ typedef void (*nova64_cartridge_status_cb_t)(uint32_t slot_idx, - bool is_inserted); + bool is_inserted); /** * @brief Callback for core log output. @@ -79,7 +91,8 @@ typedef void (*nova64_log_cb_t)(const char *message); typedef struct { nova64_display_flush_cb_t display_flush; nova64_audio_output_cb_t audio_output; - nova64_sd_read_sector_cb_t sd_read_sector; + nova64_sd_read_file_cb_t sd_read_file; + nova64_sd_get_file_size_cb_t sd_get_file_size; nova64_cartridge_status_cb_t cartridge_status; nova64_log_cb_t log_message; } nova64_io_interface_t; @@ -104,11 +117,13 @@ NOVA64_API bool nova64_init(const nova64_io_interface_t *io_interface); */ NOVA64_API int32_t nova64_main(); +#if defined(NOVA64_SHUTDOWN_AVAILABLE) /** * @brief Requests shutdown of the FreeRTOS scheduler. * @return True if the shutdown request was issued, false otherwise. */ NOVA64_API bool nova64_shutdown(void); +#endif /** * @brief Retrieves the engine version integer. diff --git a/core/include/nova64_fs.h b/core/include/nova64_fs.h new file mode 100644 index 0000000..cd77241 --- /dev/null +++ b/core/include/nova64_fs.h @@ -0,0 +1,39 @@ +#ifndef NOVA64_FS_H +#define NOVA64_FS_H + +#include +#include +#include + +/* ========================================================================== */ +/* File System API */ +/* ========================================================================== */ + +/** + * @brief Reads a file from the filesystem into a buffer. + * Supports drive identifiers: "SYS:" (internal system store), + * "EXA:" (cartridge slot 0), "EXB:" (cartridge slot 1), etc. + * @param file_path Null-terminated file path with drive identifier + * (e.g., "SYS:/init.wasm" or "EXA:/game.wasm"). + * @param buffer Pointer to destination buffer where file contents will be + * copied. + * @param buffer_size Size in bytes of the destination buffer. + * @return True if file was successfully read into buffer, false on error + * (file not found, read error, or buffer too small). + * @note Uses the sd_read_sector callback from nova64_io_interface_t. + * The callback must be registered via nova64_init() before calling + * this function. + */ +bool nova64_read_file_to_buffer(const char *file_path, uint8_t *buffer, + size_t buffer_size); + +/** + * @brief Gets the size of a file in the filesystem. + * @param file_path Null-terminated file path with drive identifier + * (e.g., "SYS:/init.wasm" or "EXA:/game.wasm"). + * @return Size of the file in bytes on success, or -1 on error + * (file not found, read error, etc.). + */ +int32_t nova64_get_file_size(const char *file_path); + +#endif // NOVA64_FS_H diff --git a/core/include/nova64_internal.h b/core/include/nova64_internal.h index a04d1db..7e3b00d 100644 --- a/core/include/nova64_internal.h +++ b/core/include/nova64_internal.h @@ -1,6 +1,10 @@ #ifndef NOVA64_INTERNAL_H #define NOVA64_INTERNAL_H +#include "nova64_core.h" + void nova64_log(const char *format, ...); +const nova64_io_interface_t *nova64_get_io_interface(void); + #endif // NOVA64_INTERNAL_H \ No newline at end of file diff --git a/core/include/wasm_runner.h b/core/include/wasm_runner.h index fc2e983..e6dc3a2 100644 --- a/core/include/wasm_runner.h +++ b/core/include/wasm_runner.h @@ -4,12 +4,6 @@ #include #include -typedef enum { - wasm_runner_role_main = 0, - wasm_runner_role_status, - wasm_runner_role_background, -} wasm_runner_role_t; - typedef struct wasm_runner_instance wasm_runner_instance_t; typedef struct { diff --git a/core/src/nova64_core.c b/core/src/nova64_core.c index 4d7ae65..708b1fd 100644 --- a/core/src/nova64_core.c +++ b/core/src/nova64_core.c @@ -1,13 +1,12 @@ #include "nova64_core.h" -#include "nova64_internal.h" #include "FreeRTOS.h" +#include "nova64_internal.h" #include "queue.h" #include "task.h" #include "wasm_runner.h" #include #include - #if defined(ESP_PLATFORM) #include "esp_pm.h" #include "esp_rom_sys.h" @@ -26,8 +25,11 @@ static const nova64_io_interface_t *g_io_interface = NULL; static QueueHandle_t event_queue = NULL; static TaskHandle_t boot_task_handle = NULL; static TaskHandle_t launcher_task_handle = NULL; + +#if defined(NOVA64_SHUTDOWN_AVAILABLE) static TaskHandle_t shutdown_task_handle = NULL; static volatile bool shutdown_requested = false; +#endif static const char *const kInternalLauncherPath = "SYS:/launcher.wasm"; @@ -83,6 +85,7 @@ static bool start_launcher_task(void) { return true; } +#if defined(NOVA64_SHUTDOWN_AVAILABLE) static void nova64_shutdown_task(void *params) { (void)params; @@ -110,6 +113,7 @@ static bool start_shutdown_task(void) { nova64_log("Shutdown task created successfully.\n"); return true; } +#endif static void nova64_boot_task(void *params) { (void)params; @@ -137,6 +141,10 @@ static void nova64_boot_task(void *params) { vTaskDelete(NULL); } +const nova64_io_interface_t *nova64_get_io_interface(void) { + return g_io_interface; +} + NOVA64_API bool nova64_init(const nova64_io_interface_t *io_interface) { if (is_initialized || io_interface == NULL) { return false; @@ -144,7 +152,10 @@ NOVA64_API bool nova64_init(const nova64_io_interface_t *io_interface) { g_io_interface_storage = *io_interface; g_io_interface = &g_io_interface_storage; + +#if defined(NOVA64_SHUTDOWN_AVAILABLE) shutdown_requested = false; +#endif nova64_log("System Initializing...\n"); @@ -154,9 +165,11 @@ NOVA64_API bool nova64_init(const nova64_io_interface_t *io_interface) { return false; } +#if defined(NOVA64_SHUTDOWN_AVAILABLE) if (!start_shutdown_task()) { return false; } +#endif nova64_log("Starting boot process...\n"); BaseType_t created = @@ -186,6 +199,7 @@ NOVA64_API int32_t nova64_main() { return 1; } +#if defined(NOVA64_SHUTDOWN_AVAILABLE) NOVA64_API bool nova64_shutdown(void) { if (!is_initialized) { return false; @@ -195,6 +209,7 @@ NOVA64_API bool nova64_shutdown(void) { shutdown_requested = true; return true; } +#endif NOVA64_API uint32_t nova64_get_version(void) { return 0x010000; // v1.0.0 diff --git a/core/src/nova64_fs.c b/core/src/nova64_fs.c new file mode 100644 index 0000000..e858bbe --- /dev/null +++ b/core/src/nova64_fs.c @@ -0,0 +1,98 @@ +#include +#include +#include +#include +#include "nova64_fs.h" +#include "nova64_core.h" +#include "nova64_internal.h" + +/* Helper for parsing the drive prefix from a path like "SYS:/file" or "EXA:/file". + * Returns the drive index on success, or -1 on failure. */ +static int parse_drive_index(const char *path) +{ + if (!path || path[0] == '\0') { + return -1; + } + + if (strncmp(path, "SYS:/", 5) == 0) { + return 0; + } + + if (strncmp(path, "EX", 2) == 0 && path[3] == ':' && path[4] == '/') { + char drive_letter = path[2]; + if (drive_letter >= 'A' && drive_letter <= 'Z') { + return drive_letter - ('A' - 1); + } + } + + return -1; +} + +bool nova64_read_file_to_buffer(const char *file_path, + uint8_t *buffer, + size_t buffer_size) +{ + if (!file_path || !buffer || buffer_size == 0) { + return false; + } + + int drive_index = parse_drive_index(file_path); + if (drive_index < 0) { + return false; + } + + const char *relative_path = file_path + 5; + if (*relative_path == '\0') { + return false; + } + + const nova64_io_interface_t *io = nova64_get_io_interface(); + if (!io || !io->sd_read_file || !io->sd_get_file_size) { + return false; + } + + /* Get the file size first */ + int32_t file_size = io->sd_get_file_size(drive_index, relative_path); + if (file_size < 0) { + return false; + } + + /* Determine how much to read */ + size_t bytes_to_read = (size_t)file_size; + if (bytes_to_read > buffer_size) { + bytes_to_read = buffer_size; + } + + /* Read the entire file into the buffer */ + int32_t bytes_read = io->sd_read_file(drive_index, relative_path, buffer, bytes_to_read); + if (bytes_read < 0) { + return false; + } + + return bytes_read > 0; +} + + +int32_t nova64_get_file_size(const char *file_path) +{ + if (!file_path) { + return -1; + } + + int drive_index = parse_drive_index(file_path); + if (drive_index < 0) { + return -1; + } + + const char *relative_path = file_path + 5; + if (*relative_path == '\0') { + return -1; + } + + const nova64_io_interface_t *io = nova64_get_io_interface(); + if (!io || !io->sd_get_file_size) { + return -1; + } + + return io->sd_get_file_size(drive_index, relative_path); +} diff --git a/core/src/wasm_runner.c b/core/src/wasm_runner.c index 730ed07..38e4d99 100644 --- a/core/src/wasm_runner.c +++ b/core/src/wasm_runner.c @@ -1,6 +1,8 @@ #include "wasm_runner.h" +#include "nova64_fs.h" #include "nova64_internal.h" #include "wasm_export.h" +#include struct wasm_runner_instance { void *native_instance; @@ -9,8 +11,40 @@ struct wasm_runner_instance { wasm_runner_instance_t *next; }; +char error_buf[128]; +uint32_t stack_size = 8092, heap_size = 8092; + bool wasm_runner_initialize(void) { + wasm_runtime_init(); nova64_log("WASM runtime initialized successfully.\n"); + + const char *module_file = "SYS:/init.wasm"; + int32_t file_size = nova64_get_file_size(module_file); + if (file_size <= 0) { + nova64_log("Failed to determine WASM module size.\n"); + return false; + } + + unsigned char *buffer = malloc((size_t)file_size); + if (!buffer) { + nova64_log("Failed to allocate WASM module buffer.\n"); + return false; + } + + if (!nova64_read_file_to_buffer(module_file, buffer, (size_t)file_size)) { + nova64_log("Failed to read WASM module into buffer.\n"); + free(buffer); + return false; + } + + wasm_module_t module = wasm_runtime_load(buffer, (uint32_t)file_size, + error_buf, sizeof(error_buf)); + + wasm_module_inst_t module_inst = wasm_runtime_instantiate( + module, stack_size, heap_size, error_buf, sizeof(error_buf)); + + wasm_exec_env_t exec_env = wasm_runtime_create_exec_env(module_inst, stack_size); + return true; } diff --git a/simulator/Nova64Core.cs b/simulator/Nova64Core.cs index 8df598d..b9109ad 100644 --- a/simulator/Nova64Core.cs +++ b/simulator/Nova64Core.cs @@ -10,8 +10,11 @@ internal static class Nova64Core [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void AudioOutputDelegate(IntPtr samples, uint count); - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate bool SdReadSectorDelegate(uint sectorIdx, IntPtr destinationBuffer, uint sectorCount); + [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + public delegate int SdReadFileDelegate(uint slotIdx, string filePath, IntPtr destinationBuffer, uint destinationLength); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + public delegate int SdGetFileSizeDelegate(uint slotIdx, string filePath); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void CartridgeStatusDelegate(uint slotIdx, bool isInserted); @@ -24,7 +27,8 @@ internal static class Nova64Core { public IntPtr display_flush; public IntPtr audio_output; - public IntPtr sd_read_sector; + public IntPtr sd_read_file; + public IntPtr sd_get_file_size; public IntPtr cartridge_status; public IntPtr log_message; } diff --git a/simulator/Nova64Simulator.csproj b/simulator/Nova64Simulator.csproj index ac07e93..5b43a0b 100644 --- a/simulator/Nova64Simulator.csproj +++ b/simulator/Nova64Simulator.csproj @@ -12,6 +12,14 @@ $(NativeLibDir) + + + + + + + + diff --git a/simulator/Program.cs b/simulator/Program.cs index 1b2b76b..58bbd1c 100644 --- a/simulator/Program.cs +++ b/simulator/Program.cs @@ -1,12 +1,77 @@ using System; +using System.IO; using System.Runtime.InteropServices; using System.Threading; +using Zio; +using Zio.FileSystems; internal static class Program { + private static readonly IFileSystem SdFileSystem = new PhysicalFileSystem(); + private static readonly UPath SdRoot = new UPath(Path.Combine(AppContext.BaseDirectory, "sd").Replace('\\', '/').Replace("C:", "/mnt/c")); + + private static UPath GetSlotFilePath(uint slotIdx, string filePath) + { + var normalizedPath = (filePath ?? string.Empty).Replace('\\', '/').TrimStart('/'); + return SdRoot / $"slot{slotIdx}" / normalizedPath; + } + private static readonly Nova64Core.DisplayFlushDelegate DisplayFlush = (buffer, width, height) => { }; private static readonly Nova64Core.AudioOutputDelegate AudioOutput = (samples, count) => { }; - private static readonly Nova64Core.SdReadSectorDelegate SdReadSector = (sectorIdx, destinationBuffer, sectorCount) => false; + private static readonly Nova64Core.SdReadFileDelegate SdReadFile = (slotIdx, filePath, destinationBuffer, destinationLength) => + { + var path = GetSlotFilePath(slotIdx, filePath); + Console.WriteLine($"[Core] SD read request: slot {slotIdx}, file '{filePath}', resolved path '{path}' destination length {destinationLength}"); + + if (destinationBuffer == IntPtr.Zero) + { + Console.WriteLine("[Core] SD read request failed: destination buffer is null."); + return -1; + } + + if (!SdFileSystem.FileExists(path)) + { + Console.WriteLine($"[Core] SD read request failed: file not found '{path}'"); + return -1; + } + + try + { + using var stream = SdFileSystem.OpenFile(path, FileMode.Open, FileAccess.Read, FileShare.Read); + var bytesToRead = (int)Math.Min(destinationLength, stream.Length); + var buffer = new byte[bytesToRead]; + var bytesRead = stream.Read(buffer, 0, bytesToRead); + Marshal.Copy(buffer, 0, destinationBuffer, bytesRead); + return bytesRead; + } + catch (Exception ex) + { + Console.WriteLine($"[Core] SD read request failed: {ex.Message}"); + return -1; + } + }; + private static readonly Nova64Core.SdGetFileSizeDelegate SdGetFileSize = (slotIdx, filePath) => + { + var path = GetSlotFilePath(slotIdx, filePath); + Console.WriteLine($"[Core] SD get file size request: slot {slotIdx}, file '{filePath}', resolved path '{path}'"); + + if (!SdFileSystem.FileExists(path)) + { + Console.WriteLine($"[Core] SD get file size failed: file not found '{path}'"); + return -1; + } + + try + { + var length = SdFileSystem.GetFileLength(path); + return length > int.MaxValue ? -1 : (int)length; + } + catch (Exception ex) + { + Console.WriteLine($"[Core] SD get file size failed: {ex.Message}"); + return -1; + } + }; private static readonly Nova64Core.CartridgeStatusDelegate CartridgeStatus = (slotIdx, isInserted) => { }; private static readonly Nova64Core.LogMessageDelegate LogMessage = (message) => { @@ -51,7 +116,8 @@ internal static class Program { display_flush = Marshal.GetFunctionPointerForDelegate(DisplayFlush), audio_output = Marshal.GetFunctionPointerForDelegate(AudioOutput), - sd_read_sector = Marshal.GetFunctionPointerForDelegate(SdReadSector), + sd_read_file = Marshal.GetFunctionPointerForDelegate(SdReadFile), + sd_get_file_size = Marshal.GetFunctionPointerForDelegate(SdGetFileSize), cartridge_status = Marshal.GetFunctionPointerForDelegate(CartridgeStatus), log_message = Marshal.GetFunctionPointerForDelegate(LogMessage) }; diff --git a/simulator/sd/slot0/init.wasm b/simulator/sd/slot0/init.wasm new file mode 100644 index 0000000..09f5299 Binary files /dev/null and b/simulator/sd/slot0/init.wasm differ diff --git a/software/init.go b/software/init.go new file mode 100644 index 0000000..39166ef --- /dev/null +++ b/software/init.go @@ -0,0 +1,15 @@ +package main + +// Declare a main function, this is the entrypoint into our go module +// That will be run. In our example, we won't need this +func main() {} + +// This exports an add function. +// It takes in two 32-bit integer values +// And returns a 32-bit integer value. +// To make this function callable from JavaScript, +// we need to add the: "export add" comment above the function +//export add +func add(x int, y int) int { + return x + y +}