Implement file system API and add shutdown functionality for desktop builds

This commit is contained in:
Lukas Höppner
2026-08-09 22:38:43 +02:00
parent 1654d366b2
commit 8e99fca404
13 changed files with 318 additions and 25 deletions
+1
View File
@@ -31,6 +31,7 @@ else()
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
message(STATUS "[Nova64] Building as Desktop Shared Library...") message(STATUS "[Nova64] Building as Desktop Shared Library...")
add_compile_definitions(NOVA64_SHUTDOWN_AVAILABLE) # Enable shutdown functionality for desktop builds
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# FreeRTOS Configuration # FreeRTOS Configuration
+27 -12
View File
@@ -1,9 +1,10 @@
#ifndef NOVA64_CORE_H #ifndef NOVA64_CORE_H
#define NOVA64_CORE_H #define NOVA64_CORE_H
#include <stdarg.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <stdarg.h>
// Cross-platform DLL export/import visibility macros // Cross-platform DLL export/import visibility macros
#if defined(_WIN32) || defined(__CYGWIN__) #if defined(_WIN32) || defined(__CYGWIN__)
@@ -42,16 +43,27 @@ typedef void (*nova64_audio_output_cb_t)(const int16_t *samples,
uint32_t count); uint32_t count);
/** /**
* @brief Callback to read sectors from the SD card. * @brief Callback to read data from the SD card.
* @param sector_idx The zero-based sector index to read. * @param slot_idx Zero-based cartridge slot index.
* @param destination_buffer Pointer to memory where sector data should be * @param file_path Null-terminated path to the file to read.
* copied. * @param destination_buffer Pointer to memory where file data should be copied.
* @param sector_count Number of 512-byte sectors to read. * @param destination_length Maximum number of bytes available in the
* @return True on success, false on read error. * 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, typedef int32_t (*nova64_sd_read_file_cb_t)(uint32_t slot_idx,
uint8_t *destination_buffer, const char *file_path,
uint32_t sector_count); 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. * @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. * @param is_inserted True if a cartridge is detected, false if ejected.
*/ */
typedef void (*nova64_cartridge_status_cb_t)(uint32_t slot_idx, typedef void (*nova64_cartridge_status_cb_t)(uint32_t slot_idx,
bool is_inserted); bool is_inserted);
/** /**
* @brief Callback for core log output. * @brief Callback for core log output.
@@ -79,7 +91,8 @@ typedef void (*nova64_log_cb_t)(const char *message);
typedef struct { typedef struct {
nova64_display_flush_cb_t display_flush; nova64_display_flush_cb_t display_flush;
nova64_audio_output_cb_t audio_output; 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_cartridge_status_cb_t cartridge_status;
nova64_log_cb_t log_message; nova64_log_cb_t log_message;
} nova64_io_interface_t; } 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(); NOVA64_API int32_t nova64_main();
#if defined(NOVA64_SHUTDOWN_AVAILABLE)
/** /**
* @brief Requests shutdown of the FreeRTOS scheduler. * @brief Requests shutdown of the FreeRTOS scheduler.
* @return True if the shutdown request was issued, false otherwise. * @return True if the shutdown request was issued, false otherwise.
*/ */
NOVA64_API bool nova64_shutdown(void); NOVA64_API bool nova64_shutdown(void);
#endif
/** /**
* @brief Retrieves the engine version integer. * @brief Retrieves the engine version integer.
+39
View File
@@ -0,0 +1,39 @@
#ifndef NOVA64_FS_H
#define NOVA64_FS_H
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
/* ========================================================================== */
/* 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
+4
View File
@@ -1,6 +1,10 @@
#ifndef NOVA64_INTERNAL_H #ifndef NOVA64_INTERNAL_H
#define NOVA64_INTERNAL_H #define NOVA64_INTERNAL_H
#include "nova64_core.h"
void nova64_log(const char *format, ...); void nova64_log(const char *format, ...);
const nova64_io_interface_t *nova64_get_io_interface(void);
#endif // NOVA64_INTERNAL_H #endif // NOVA64_INTERNAL_H
-6
View File
@@ -4,12 +4,6 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
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 wasm_runner_instance wasm_runner_instance_t;
typedef struct { typedef struct {
+17 -2
View File
@@ -1,13 +1,12 @@
#include "nova64_core.h" #include "nova64_core.h"
#include "nova64_internal.h"
#include "FreeRTOS.h" #include "FreeRTOS.h"
#include "nova64_internal.h"
#include "queue.h" #include "queue.h"
#include "task.h" #include "task.h"
#include "wasm_runner.h" #include "wasm_runner.h"
#include <stdarg.h> #include <stdarg.h>
#include <stdio.h> #include <stdio.h>
#if defined(ESP_PLATFORM) #if defined(ESP_PLATFORM)
#include "esp_pm.h" #include "esp_pm.h"
#include "esp_rom_sys.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 QueueHandle_t event_queue = NULL;
static TaskHandle_t boot_task_handle = NULL; static TaskHandle_t boot_task_handle = NULL;
static TaskHandle_t launcher_task_handle = NULL; static TaskHandle_t launcher_task_handle = NULL;
#if defined(NOVA64_SHUTDOWN_AVAILABLE)
static TaskHandle_t shutdown_task_handle = NULL; static TaskHandle_t shutdown_task_handle = NULL;
static volatile bool shutdown_requested = false; static volatile bool shutdown_requested = false;
#endif
static const char *const kInternalLauncherPath = "SYS:/launcher.wasm"; static const char *const kInternalLauncherPath = "SYS:/launcher.wasm";
@@ -83,6 +85,7 @@ static bool start_launcher_task(void) {
return true; return true;
} }
#if defined(NOVA64_SHUTDOWN_AVAILABLE)
static void nova64_shutdown_task(void *params) { static void nova64_shutdown_task(void *params) {
(void)params; (void)params;
@@ -110,6 +113,7 @@ static bool start_shutdown_task(void) {
nova64_log("Shutdown task created successfully.\n"); nova64_log("Shutdown task created successfully.\n");
return true; return true;
} }
#endif
static void nova64_boot_task(void *params) { static void nova64_boot_task(void *params) {
(void)params; (void)params;
@@ -137,6 +141,10 @@ static void nova64_boot_task(void *params) {
vTaskDelete(NULL); 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) { NOVA64_API bool nova64_init(const nova64_io_interface_t *io_interface) {
if (is_initialized || io_interface == NULL) { if (is_initialized || io_interface == NULL) {
return false; 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_storage = *io_interface;
g_io_interface = &g_io_interface_storage; g_io_interface = &g_io_interface_storage;
#if defined(NOVA64_SHUTDOWN_AVAILABLE)
shutdown_requested = false; shutdown_requested = false;
#endif
nova64_log("System Initializing...\n"); nova64_log("System Initializing...\n");
@@ -154,9 +165,11 @@ NOVA64_API bool nova64_init(const nova64_io_interface_t *io_interface) {
return false; return false;
} }
#if defined(NOVA64_SHUTDOWN_AVAILABLE)
if (!start_shutdown_task()) { if (!start_shutdown_task()) {
return false; return false;
} }
#endif
nova64_log("Starting boot process...\n"); nova64_log("Starting boot process...\n");
BaseType_t created = BaseType_t created =
@@ -186,6 +199,7 @@ NOVA64_API int32_t nova64_main() {
return 1; return 1;
} }
#if defined(NOVA64_SHUTDOWN_AVAILABLE)
NOVA64_API bool nova64_shutdown(void) { NOVA64_API bool nova64_shutdown(void) {
if (!is_initialized) { if (!is_initialized) {
return false; return false;
@@ -195,6 +209,7 @@ NOVA64_API bool nova64_shutdown(void) {
shutdown_requested = true; shutdown_requested = true;
return true; return true;
} }
#endif
NOVA64_API uint32_t nova64_get_version(void) { NOVA64_API uint32_t nova64_get_version(void) {
return 0x010000; // v1.0.0 return 0x010000; // v1.0.0
+98
View File
@@ -0,0 +1,98 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#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);
}
+34
View File
@@ -1,6 +1,8 @@
#include "wasm_runner.h" #include "wasm_runner.h"
#include "nova64_fs.h"
#include "nova64_internal.h" #include "nova64_internal.h"
#include "wasm_export.h" #include "wasm_export.h"
#include <stdlib.h>
struct wasm_runner_instance { struct wasm_runner_instance {
void *native_instance; void *native_instance;
@@ -9,8 +11,40 @@ struct wasm_runner_instance {
wasm_runner_instance_t *next; wasm_runner_instance_t *next;
}; };
char error_buf[128];
uint32_t stack_size = 8092, heap_size = 8092;
bool wasm_runner_initialize(void) { bool wasm_runner_initialize(void) {
wasm_runtime_init(); wasm_runtime_init();
nova64_log("WASM runtime initialized successfully.\n"); 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; return true;
} }
+7 -3
View File
@@ -10,8 +10,11 @@ internal static class Nova64Core
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void AudioOutputDelegate(IntPtr samples, uint count); public delegate void AudioOutputDelegate(IntPtr samples, uint count);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public delegate bool SdReadSectorDelegate(uint sectorIdx, IntPtr destinationBuffer, uint sectorCount); 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)] [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void CartridgeStatusDelegate(uint slotIdx, bool isInserted); public delegate void CartridgeStatusDelegate(uint slotIdx, bool isInserted);
@@ -24,7 +27,8 @@ internal static class Nova64Core
{ {
public IntPtr display_flush; public IntPtr display_flush;
public IntPtr audio_output; 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 cartridge_status;
public IntPtr log_message; public IntPtr log_message;
} }
+8
View File
@@ -12,6 +12,14 @@
<NativeLibDir>$(NativeLibDir)</NativeLibDir> <NativeLibDir>$(NativeLibDir)</NativeLibDir>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Zio" Version="0.24.0" />
</ItemGroup>
<ItemGroup>
<Content Include="sd\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup Condition="'$(NativeLibDir)' != ''"> <ItemGroup Condition="'$(NativeLibDir)' != ''">
<Content Include="$(NativeLibDir)\nova64_core.dll" Condition="Exists('$(NativeLibDir)\nova64_core.dll')" CopyToOutputDirectory="IfDifferent" /> <Content Include="$(NativeLibDir)\nova64_core.dll" Condition="Exists('$(NativeLibDir)\nova64_core.dll')" CopyToOutputDirectory="IfDifferent" />
<Content Include="$(NativeLibDir)\libnova64_core.so" Condition="Exists('$(NativeLibDir)\libnova64_core.so')" CopyToOutputDirectory="IfDifferent" /> <Content Include="$(NativeLibDir)\libnova64_core.so" Condition="Exists('$(NativeLibDir)\libnova64_core.so')" CopyToOutputDirectory="IfDifferent" />
+68 -2
View File
@@ -1,12 +1,77 @@
using System; using System;
using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using Zio;
using Zio.FileSystems;
internal static class Program 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.DisplayFlushDelegate DisplayFlush = (buffer, width, height) => { };
private static readonly Nova64Core.AudioOutputDelegate AudioOutput = (samples, count) => { }; 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.CartridgeStatusDelegate CartridgeStatus = (slotIdx, isInserted) => { };
private static readonly Nova64Core.LogMessageDelegate LogMessage = (message) => private static readonly Nova64Core.LogMessageDelegate LogMessage = (message) =>
{ {
@@ -51,7 +116,8 @@ internal static class Program
{ {
display_flush = Marshal.GetFunctionPointerForDelegate(DisplayFlush), display_flush = Marshal.GetFunctionPointerForDelegate(DisplayFlush),
audio_output = Marshal.GetFunctionPointerForDelegate(AudioOutput), 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), cartridge_status = Marshal.GetFunctionPointerForDelegate(CartridgeStatus),
log_message = Marshal.GetFunctionPointerForDelegate(LogMessage) log_message = Marshal.GetFunctionPointerForDelegate(LogMessage)
}; };
Binary file not shown.
+15
View File
@@ -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
}