Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,065 changes: 965 additions & 100 deletions Cargo.lock

Large diffs are not rendered by default.

28 changes: 15 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ authors = ["the Andromeda team"]
edition = "2024"
license = "Mozilla Public License 2.0"
repository = "https://github.com/tryandromeda/andromeda"
version = "0.1.5"
version = "0.1.6"

[workspace.dependencies]
andromeda-core = { path = "crates/core" }
Expand All @@ -25,24 +25,24 @@ chrono = { version = "0.4.44", features = ["serde"] }
clap = { version = "4.6.0", features = ["derive"] }
clap_complete = "4.6.0"
console = "0.16.3"
toml = "1.1.0"
toml = "1.1.2"
copilot-client = "0.1.0"
cosmic-text = "0.18.2"
dprint-core = "0.67.4"
dprint-plugin-typescript = "0.95.15"
dprint-plugin-json = "0.21.3"
env_logger = "0.11.9"
env_logger = "0.11.10"
futures = "0.3.32"
glob = "0.3.3"
hotpath = { version = "0.14" }
indexmap = "2.13.0"
hotpath = { version = "0.15" }
indexmap = "2.14.0"
image = "0.25.10"
lazy_static = "1.5.0"
libloading = "0.9.0"
libffi = "5.1.0"
libsui = "0.13.0"
libsui = "0.14.0"
log = "0.4.29"
lru = "0.16.3"
lru = "0.17.0"
lsp-types = "0.97.0"
nova_vm = { git = "https://github.com/trynova/nova", rev = "a82b0408533bc93f857aa2ee5daee4f39f62dc6f" }
nu-ansi-term = "0.50.3"
Expand All @@ -58,10 +58,10 @@ oxc_parser = "0.122.0"
oxc_semantic = "0.122.0"
oxc_span = "0.122.0"
oxc_transformer = "0.122.0"
rand = "0.10.0"
rand = "0.10.1"
reedline = "0.46.0"
regex = "1.12.3"
rustls = "0.23.37"
rustls = "0.23.38"
rustls-pemfile = "2.2.0"
rustls-pki-types = "1.14.0"
ring = "0.17.14"
Expand All @@ -78,18 +78,20 @@ socket2 = "0.6.3"

swash = "0.2.6"
trust-dns-resolver = "0.23.2"
signal-hook = "0.4.3"
signal-hook = "0.4.4"
thiserror = "2.0.18"
tempfile = "3.27.0"
tokio = { version = "1.50.0", features = ["rt", "sync", "time", "fs"] }
tokio = { version = "1.52.1", features = ["rt", "sync", "time", "fs"] }
tokio-rustls = "0.26.4"
tokio-test = "0.4.5"
tower-lsp = "0.20.0"
ureq = { version = "3.3.0", features = ["json"] }
url = { version = "2.5.8", features = ["serde", "expose_internals"] }
uuid = { version = "1.22.0", features = ["v4"] }
webpki-roots = "1.0.6"
uuid = { version = "1.23.1", features = ["v4"] }
webpki-roots = "1.0.7"
wgpu = { version = "27.0.1", features = ["wgsl", "webgpu"] }
winit = "0.30"
raw-window-handle = "0.6"

[profile.dev]
split-debuginfo = "packed"
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,38 @@ or disabled as needed:
| **Time** | Timing utilities | `performance.now()`, `setTimeout()`, `setInterval()`, `Andromeda.sleep()` |
| **URL** | URL parsing and manipulation | `URL`, `URLSearchParams` |
| **Web** | Web standards | `TextEncoder`, `TextDecoder`, `navigator`, `queueMicrotask()` |
| **Window** *(optional)* | Native OS windowing (winit) | `Andromeda.Window`, `Andromeda.createWindow()`, DOM-style events, `rawHandle()` |

### Window extension (optional, behind `window` feature)

The `window` feature adds `Andromeda.Window` — a native OS window backed by
[`winit`](https://crates.io/crates/winit) on macOS, Windows, and Linux
(X11/Wayland). Inspired by [`deno-windowing/dwm`](https://github.com/deno-windowing/dwm),
the class extends `EventTarget` and dispatches DOM-style events (`resize`,
`close`, `keydown`, `keyup`, `mousemove`, `mousedown`, `mouseup`).

```ts
const win = Andromeda.createWindow({ title: "Hello", width: 640, height: 480 });
win.addEventListener("keydown", (e) => {
if ((e as CustomEvent).detail.code === "Escape") win.close();
});
await Andromeda.Window.mainloop();
```

Enable the feature:

```bash
cargo run --features window -- run examples/window.ts
```

`window.rawHandle()` returns a `{ system, windowHandle, displayHandle, width, height }`
object compatible with `Deno.UnsafeWindowSurface` for future WebGPU-surface bridges.

When the `canvas` feature is also enabled, `window.presentCanvas(canvas)` blits an
`OffscreenCanvas`'s latest frame into the window via a shared wgpu device — no CPU
readback, any size allowed. `examples/window.ts` demonstrates a live 2D scene streamed
from an `OffscreenCanvas` into a winit-backed window. `examples/breakout.ts` is a
keyboard-driven Breakout clone exercising the full window + canvas input/render pipeline.

## Andromeda Satellites

Expand Down
3 changes: 2 additions & 1 deletion crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ repository.workspace = true
readme = "../../README.md"

[features]
default = []
default = ["window"]
hotpath = [
"hotpath/hotpath",
"andromeda-core/hotpath",
"andromeda-runtime/hotpath",
]
hotpath-alloc = ["hotpath/hotpath-alloc"]
llm = ["andromeda-core/llm"]
window = ["andromeda-runtime/window"]

[lib]
name = "andromeda"
Expand Down
10 changes: 10 additions & 0 deletions crates/cli/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,16 @@ impl From<RuntimeError> for CliError {
source_code: None,
error_span: None,
},
RuntimeError::WindowError {
operation, message, ..
} => CliError::RuntimeError {
message: format!("Window error during {}: {}", operation, message),
file_path: None,
line: None,
column: None,
source_code: None,
error_span: None,
},
RuntimeError::InternalError { message, .. } => CliError::RuntimeError {
message: format!("Internal error: {}", message),
file_path: None,
Expand Down
18 changes: 6 additions & 12 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ fn run_main() -> CliResult<()> {
Cli::parse()
};

let rt = tokio::runtime::Builder::new_current_thread()
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_time()
.enable_io()
.build()
Expand All @@ -301,9 +301,9 @@ fn run_main() -> CliResult<()> {
Some(Box::new(e)),
)
})?;
let _tokio_guard = rt.enter();

// Run Nova in a secondary blocking thread so tokio tasks can still run
let nova_thread = rt.spawn_blocking(move || -> CliResult<()> {
let nova_result: CliResult<()> = (move || -> CliResult<()> {
match args.command {
Command::Run {
verbose,
Expand Down Expand Up @@ -491,15 +491,9 @@ fn run_main() -> CliResult<()> {
Command::Task { task_name } => run_task(task_name).map_err(|e| *e),
Command::Config { action } => handle_config_command(action),
}
});
match rt.block_on(nova_thread) {
Ok(result) => result,
Err(e) => Err(error::CliError::config_error(
"Runtime execution failed".to_string(),
None,
Some(Box::new(e)),
)),
}
})();
drop(_tokio_guard);
nova_result
}

fn generate_completions(shell: Option<Shell>) {
Expand Down
1 change: 1 addition & 0 deletions crates/cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ pub fn run_with_config(
eventloop_handler: recommended_eventloop_handler,
macro_task_rx,
import_map,
pre_tick_hook: andromeda_runtime::recommended_pre_tick_hook(),
},
host_data,
);
Expand Down
37 changes: 36 additions & 1 deletion crates/core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,23 @@ pub enum RuntimeError {
source_code: Option<NamedSource<String>>,
},

/// Windowing / native window subsystem errors (winit-backed extension)
#[diagnostic(
code(andromeda::window::error),
help(
"🔍 Check that the window feature is enabled and the window hasn't been closed.\n💡 Verify the platform supports native windowing.\n🪟 Ensure window operations run on the main thread."
),
url("https://docs.andromeda.dev/window")
)]
WindowError {
operation: String,
message: String,
#[label("🪟 Window operation failed here")]
error_location: Option<SourceSpan>,
#[source_code]
source_code: Option<NamedSource<String>>,
},

/// Internal error (should not happen in normal operation)
#[diagnostic(
code(andromeda::internal::error),
Expand Down Expand Up @@ -1220,7 +1237,15 @@ impl RuntimeError {
}
}

// -------------------- Internal Errors --------------------
/// Create a new window error
pub fn window_error(operation: impl Into<String>, message: impl Into<String>) -> Self {
Self::WindowError {
operation: operation.into(),
message: message.into(),
error_location: None,
source_code: None,
}
}

/// Create an internal error
pub fn internal_error(message: impl Into<String>) -> Self {
Expand Down Expand Up @@ -1341,6 +1366,11 @@ impl fmt::Display for RuntimeError {
RuntimeError::LlmNetworkError { message, .. } => {
write!(f, "LLM network error: {message}")
}
RuntimeError::WindowError {
operation, message, ..
} => {
write!(f, "Window error during {operation}: {message}")
}
RuntimeError::InternalError { message, .. } => {
write!(f, "Internal error: {message}")
}
Expand Down Expand Up @@ -1798,6 +1828,11 @@ macro_rules! runtime_error {
$crate::RuntimeError::llm_network_error($msg)
};

// Window errors
(window: $op:expr, $msg:expr) => {
$crate::RuntimeError::window_error($op, $msg)
};

// Internal errors
(internal: $msg:expr) => {
$crate::RuntimeError::internal_error($msg)
Expand Down
14 changes: 14 additions & 0 deletions crates/core/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,11 @@ impl RuntimeFile {
}
}

/// Called once per iteration of the main `Runtime::run` loop, after promise
/// jobs and timeouts have been drained and before the runtime blocks waiting
/// on macro tasks.
pub type PreTickHook<UserMacroTask> = Box<dyn Fn(&HostData<UserMacroTask>) + 'static>;

pub struct RuntimeConfig<UserMacroTask: 'static> {
/// Disable or not strict mode.
pub no_strict: bool,
Expand All @@ -666,6 +671,9 @@ pub struct RuntimeConfig<UserMacroTask: 'static> {
pub macro_task_rx: Receiver<MacroTask<UserMacroTask>>,
/// Import map for module resolution
pub import_map: Option<ImportMap>,
/// Optional per-iteration hook invoked inside the main run loop. See
/// [`PreTickHook`] for semantics and threading constraints.
pub pre_tick_hook: Option<PreTickHook<UserMacroTask>>,
}

pub struct Runtime<UserMacroTask: 'static> {
Expand Down Expand Up @@ -860,6 +868,12 @@ impl<UserMacroTask> Runtime<UserMacroTask> {
self.host_hooks.drain_ready_timeout_jobs();
}

// Pump any extension-owned event loop (e.g. winit) before we
// consider blocking on macro tasks.
if let Some(ref hook) = self.config.pre_tick_hook {
hook(&self.host_hooks.host_data);
}

// Try to handle a macro task without blocking
// This handles the case where a task completed so fast that the counter
// was already decremented but the message is still in the channel
Expand Down
3 changes: 3 additions & 0 deletions crates/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ crypto = ["dep:ring", "dep:rand"]
storage = ["dep:rusqlite"]
virtualfs = ["storage"]
serve = []
window = ["dep:winit", "dep:raw-window-handle", "dep:wgpu"]
hotpath = ["hotpath/hotpath", "andromeda-core/hotpath"]
hotpath-alloc = ["hotpath/hotpath-alloc"]
typescript = ["nova_vm/typescript"]
Expand Down Expand Up @@ -64,6 +65,8 @@ libffi.workspace = true
thiserror.workspace = true
uuid.workspace = true
wgpu = { workspace = true, optional = true }
winit = { workspace = true, optional = true }
raw-window-handle = { workspace = true, optional = true }
rustls.workspace = true
tokio-rustls.workspace = true
webpki-roots.workspace = true
Expand Down
58 changes: 56 additions & 2 deletions crates/runtime/src/ext/canvas/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,7 @@ impl CanvasExt {
.get_host_data()
.downcast_ref::<HostData<crate::RuntimeMacroTask>>()
.unwrap();
let (device, queue) = acquire_device_and_queue(host_data);
let mut storage = host_data.storage.borrow_mut();
let res: &mut CanvasResources = storage.get_mut().unwrap(); // Create canvas data
let canvas_rid = res.canvases.push(CanvasData {
Expand Down Expand Up @@ -946,8 +947,8 @@ impl CanvasExt {
direction: state::Direction::default(),
});

// Create renderer with GPU device.
let (device, queue) = create_wgpu_device_sync();
// Renderer uses the wgpu device acquired above (shared with the
// window extension when both features are enabled).
let dimensions = renderer::Dimensions { width, height };
let format = wgpu::TextureFormat::Bgra8Unorm;
let renderer = renderer::Renderer::new(device, queue, format, dimensions);
Expand Down Expand Up @@ -4782,6 +4783,59 @@ fn extract_image_region(
result
}

/// Flush pending canvas commands for the given canvas rid and return a
/// clone of its `resolve_target` texture. Clones are cheap — `wgpu::Texture`
/// is internally `Arc`-ed. Used by the `window` extension's canvas bridge
/// to sample the latest canvas frame in a blit render pass. Takes the raw
/// `OpsStorage` borrow to sidestep the `'gc` lifetime on `CanvasResources`.
#[cfg(feature = "window")]
pub(crate) fn render_canvas_to_texture(
storage: &mut OpsStorage,
canvas_rid: Rid,
) -> Option<wgpu::Texture> {
let res: &mut CanvasResources = storage.get_mut()?;
let mut renderer = res.renderers.get_mut(canvas_rid)?;
renderer.render_all();
Some(renderer.resolve_target.clone())
}

/// Acquire a wgpu device+queue for a new canvas. When the `window` feature
/// is enabled the shared `WindowingGpu` is used so canvas textures live on
/// the same device as window surfaces — that's what lets `presentCanvas`
/// blit a canvas frame into a window without cross-device copies. If the
/// WindowingState slot is present but `ensure_gpu()` fails, we surface
/// that error as a panic rather than silently creating a divergent
/// standalone device — a mismatch would otherwise manifest as an opaque
/// wgpu validation error at the next `presentCanvas` call. When the
/// `window` feature is compiled out we fall back to stand-alone device
/// creation, which has always been the canvas default.
fn acquire_device_and_queue(
host_data: &HostData<crate::RuntimeMacroTask>,
) -> (wgpu::Device, wgpu::Queue) {
#[cfg(feature = "window")]
{
let mut storage = host_data.storage.borrow_mut();
if let Some(state) = storage.get_mut::<crate::ext::window::state::WindowingState>() {
match state.ensure_gpu() {
Ok(gpu) => return (gpu.device.clone(), gpu.queue.clone()),
Err(e) => {
// Loud failure keeps the error close to the root cause.
// Silent fallback would produce a second, independent
// wgpu::Device; any later bridge call would then crash
// with a device-mismatch error with no backtrace to
// this moment.
panic!("[andromeda/canvas] shared WindowingGpu init failed: {e}");
}
}
}
}
#[cfg(not(feature = "window"))]
{
let _ = host_data; // silence unused-parameter warning
}
create_wgpu_device_sync()
}

fn create_wgpu_device_sync() -> (wgpu::Device, wgpu::Queue) {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
Expand Down
Loading
Loading