From b6f6d908d2a4da9088268109e59024688c920ac7 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 24 Jun 2023 17:49:41 -0700 Subject: [PATCH 1/5] [oden] OK now everything is TS and it type-checks --- oden-js/src/context.rs | 2 +- src/graphics.ts | 6 +- src/{main.js => main.ts} | 2 +- src/script.rs | 160 ++------------------------------------- src/script/typescript.rs | 154 +++++++++++++++++++++++++++++++++++++ types/graphics-core.d.ts | 2 + 6 files changed, 168 insertions(+), 158 deletions(-) rename src/{main.js => main.ts} (75%) create mode 100644 src/script/typescript.rs diff --git a/oden-js/src/context.rs b/oden-js/src/context.rs index 300a5915..22f75654 100644 --- a/oden-js/src/context.rs +++ b/oden-js/src/context.rs @@ -151,7 +151,7 @@ impl ContextRef { pub fn import_module(&self, name: &str, base: &str) -> Result { let name = CString::new(name)?; let base = CString::new(base)?; - let module = unsafe { sys::JS_RunModule(self.ctx, name.as_ptr(), base.as_ptr()) }; + let module = unsafe { sys::JS_RunModule(self.ctx, base.as_ptr(), name.as_ptr()) }; if module.is_null() { return Err(self.exception_error()); } diff --git a/src/graphics.ts b/src/graphics.ts index 944f99c3..b5c067c6 100644 --- a/src/graphics.ts +++ b/src/graphics.ts @@ -1,13 +1,17 @@ import * as core from "graphics-core"; +// Clear the screen to the specified color. r, g, and b should vary from 0 to +// 1. export function cls(r: number, g: number, b: number) { core.cls(r, g, b); } -export function print(...args: string[]) { +// Print a message to the console. +export function print(...args: unknown[]) { core.print(args.join(" ")); } +// Draw a sprite in the rectangle from x,y to export function spr( x: number, y: number, diff --git a/src/main.js b/src/main.ts similarity index 75% rename from src/main.js rename to src/main.ts index fd8099f1..46ad15b9 100644 --- a/src/main.js +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { cls, print, spr } from "./src/graphics.ts"; +import { cls, print, spr } from "./graphics.ts"; export function init() { print("Hello world!"); diff --git a/src/script.rs b/src/script.rs index 80e4cb5a..dc761730 100644 --- a/src/script.rs +++ b/src/script.rs @@ -1,166 +1,16 @@ use oden_js::{ module::loader::{ModuleLoader, ModuleSource}, - Context, ContextRef, Error, Result, Runtime, Value, + Context, ContextRef, Result, Runtime, Value, }; -use std::cell::RefCell; use std::ffi::OsStr; use std::path::Path; -use std::rc::Rc; use std::sync::mpsc::{channel, Receiver}; -use swc_common::{ - self, comments::SingleThreadedComments, errors::Diagnostic, sync::Lrc, FileName, Globals, Mark, - SourceMap, GLOBALS, -}; -use swc_ecma_codegen::{text_writer::JsWriter, Emitter}; -use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax}; -use swc_ecma_transforms_base::{fixer::fixer, hygiene::hygiene, resolver}; -use swc_ecma_transforms_typescript::strip; -use swc_ecma_visit::FoldWith; - pub mod graphics; use graphics::GraphicsCommand; -struct DiagnosticCollector { - cell: Rc>>, -} - -impl DiagnosticCollector { - pub fn into_handler(self) -> swc_common::errors::Handler { - swc_common::errors::Handler::with_emitter(true, false, Box::new(self)) - } -} - -impl swc_common::errors::Emitter for DiagnosticCollector { - fn emit(&mut self, db: &swc_common::errors::DiagnosticBuilder<'_>) { - use std::ops::Deref; - self.cell.borrow_mut().push(db.deref().clone()); - } -} - -fn is_fatal_swc_diagnostic(diagnostic: &Diagnostic) -> bool { - use swc_common::errors::Level; - match diagnostic.level { - Level::Bug - | Level::Cancelled - | Level::FailureNote - | Level::Fatal - | Level::PhaseFatal - | Level::Error => true, - Level::Help | Level::Note | Level::Warning => false, - } -} -fn format_swc_diagnostic(source_map: &SourceMap, diagnostic: &Diagnostic) -> String { - if let Some(span) = &diagnostic.span.primary_span() { - let file_name = source_map.span_to_filename(*span); - let loc = source_map.lookup_char_pos(span.lo); - format!( - "{} at {}:{}:{}", - diagnostic.message(), - file_name, - loc.line, - loc.col_display + 1, - ) - } else { - diagnostic.message() - } -} - -fn ensure_no_fatal_swc_diagnostics<'a>( - name: &str, - source_map: &SourceMap, - diagnostics: impl Iterator, -) -> Result<()> { - let fatal_diagnostics = diagnostics - .filter(|d| is_fatal_swc_diagnostic(d)) - .collect::>(); - if !fatal_diagnostics.is_empty() { - Err(Error::ParseError( - name.into(), - fatal_diagnostics - .iter() - .map(|d| format_swc_diagnostic(source_map, d)) - .collect::>() - .join("\n\n"), - )) - } else { - Ok(()) - } -} - -fn transpile_to_javascript(path: &str, input: String) -> Result { - // NOTE: This was taken almost verbatim from - // https://github.com/swc-project/swc/blob/main/crates/swc_ecma_transforms_typescript/examples/ts_to_js.rs - // This appears to be similar to what deno_ast does, but this has - // the advantage of actually compiling. :P - let cm: Lrc = Default::default(); - let diagnostics_cell: Rc>> = Rc::new(RefCell::new(Vec::new())); - let handler = DiagnosticCollector { - cell: diagnostics_cell.clone(), - } - .into_handler(); - - let fm = cm.new_source_file(FileName::Custom(path.into()), input.into()); - let comments = SingleThreadedComments::default(); - - let lexer = Lexer::new( - Syntax::Typescript(Default::default()), - Default::default(), - StringInput::from(&*fm), - Some(&comments), - ); - - let mut parser = Parser::new_from(lexer); - - for e in parser.take_errors() { - e.into_diagnostic(&handler).emit(); - } - - let module = parser - .parse_module() - .map_err(|e| e.into_diagnostic(&handler).emit()) - .expect("failed to parse module."); - - let globals = Globals::default(); - GLOBALS.set(&globals, || { - let unresolved_mark = Mark::new(); - let top_level_mark = Mark::new(); - - // Optionally transforms decorators here before the resolver pass - // as it might produce runtime declarations. - - // Conduct identifier scope analysis - let module = module.fold_with(&mut resolver(unresolved_mark, top_level_mark, true)); - - // Remove typescript types - let module = module.fold_with(&mut strip(top_level_mark)); - - // Fix up any identifiers with the same name, but different contexts - let module = module.fold_with(&mut hygiene()); - - // Ensure that we have enough parenthesis. - let module = module.fold_with(&mut fixer(Some(&comments))); - - let mut buf = vec![]; - { - let mut emitter = Emitter { - cfg: swc_ecma_codegen::Config { - minify: false, - ..Default::default() - }, - cm: cm.clone(), - comments: Some(&comments), - wr: JsWriter::new(cm.clone(), "\n", &mut buf, None), - }; - - emitter.emit_module(&module).unwrap(); - } - - let diagnostics = diagnostics_cell.borrow(); - ensure_no_fatal_swc_diagnostics(path, &cm, diagnostics.iter())?; - Ok(String::from_utf8(buf).expect("non-utf8?")) - }) -} +mod typescript; +use typescript::transpile_to_javascript; struct Loader {} @@ -172,6 +22,7 @@ impl Loader { impl ModuleLoader for Loader { fn load(&self, _context: &ContextRef, name: &str) -> Result { + eprintln!("Loading {name}..."); let path = Path::new(name); let contents = std::fs::read_to_string(path)?; let contents = if path.extension().and_then(OsStr::to_str) == Some("ts") { @@ -208,9 +59,8 @@ impl ScriptContext { let gfx = graphics::GraphicsAPI::define(&context, gfx_send) .expect("Graphics module should load without error"); - let js = include_str!("main.js"); let module = context - .eval_module(js, "main.js") + .import_module("./src/main.js", "") .expect("Unable to load main"); let init = module diff --git a/src/script/typescript.rs b/src/script/typescript.rs new file mode 100644 index 00000000..19bfc75b --- /dev/null +++ b/src/script/typescript.rs @@ -0,0 +1,154 @@ +use oden_js::{Error, Result}; +use std::cell::RefCell; +use std::rc::Rc; + +use swc_common::{ + self, comments::SingleThreadedComments, errors::Diagnostic, sync::Lrc, FileName, Globals, Mark, + SourceMap, GLOBALS, +}; +use swc_ecma_codegen::{text_writer::JsWriter, Emitter}; +use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax}; +use swc_ecma_transforms_base::{fixer::fixer, hygiene::hygiene, resolver}; +use swc_ecma_transforms_typescript::strip; +use swc_ecma_visit::FoldWith; + +struct DiagnosticCollector { + cell: Rc>>, +} + +impl DiagnosticCollector { + pub fn into_handler(self) -> swc_common::errors::Handler { + swc_common::errors::Handler::with_emitter(true, false, Box::new(self)) + } +} + +impl swc_common::errors::Emitter for DiagnosticCollector { + fn emit(&mut self, db: &swc_common::errors::DiagnosticBuilder<'_>) { + use std::ops::Deref; + self.cell.borrow_mut().push(db.deref().clone()); + } +} + +fn is_fatal_swc_diagnostic(diagnostic: &Diagnostic) -> bool { + use swc_common::errors::Level; + match diagnostic.level { + Level::Bug + | Level::Cancelled + | Level::FailureNote + | Level::Fatal + | Level::PhaseFatal + | Level::Error => true, + Level::Help | Level::Note | Level::Warning => false, + } +} +fn format_swc_diagnostic(source_map: &SourceMap, diagnostic: &Diagnostic) -> String { + if let Some(span) = &diagnostic.span.primary_span() { + let file_name = source_map.span_to_filename(*span); + let loc = source_map.lookup_char_pos(span.lo); + format!( + "{} at {}:{}:{}", + diagnostic.message(), + file_name, + loc.line, + loc.col_display + 1, + ) + } else { + diagnostic.message() + } +} + +fn ensure_no_fatal_swc_diagnostics<'a>( + name: &str, + source_map: &SourceMap, + diagnostics: impl Iterator, +) -> Result<()> { + let fatal_diagnostics = diagnostics + .filter(|d| is_fatal_swc_diagnostic(d)) + .collect::>(); + if !fatal_diagnostics.is_empty() { + Err(Error::ParseError( + name.into(), + fatal_diagnostics + .iter() + .map(|d| format_swc_diagnostic(source_map, d)) + .collect::>() + .join("\n\n"), + )) + } else { + Ok(()) + } +} + +pub fn transpile_to_javascript(path: &str, input: String) -> Result { + // NOTE: This was taken almost verbatim from + // https://github.com/swc-project/swc/blob/main/crates/swc_ecma_transforms_typescript/examples/ts_to_js.rs + // This appears to be similar to what deno_ast does, but this has + // the advantage of actually compiling. :P + let cm: Lrc = Default::default(); + let diagnostics_cell: Rc>> = Rc::new(RefCell::new(Vec::new())); + let handler = DiagnosticCollector { + cell: diagnostics_cell.clone(), + } + .into_handler(); + + let fm = cm.new_source_file(FileName::Custom(path.into()), input.into()); + let comments = SingleThreadedComments::default(); + + let lexer = Lexer::new( + Syntax::Typescript(Default::default()), + Default::default(), + StringInput::from(&*fm), + Some(&comments), + ); + + let mut parser = Parser::new_from(lexer); + + for e in parser.take_errors() { + e.into_diagnostic(&handler).emit(); + } + + let module = parser + .parse_module() + .map_err(|e| e.into_diagnostic(&handler).emit()) + .expect("failed to parse module."); + + let globals = Globals::default(); + GLOBALS.set(&globals, || { + let unresolved_mark = Mark::new(); + let top_level_mark = Mark::new(); + + // Optionally transforms decorators here before the resolver pass + // as it might produce runtime declarations. + + // Conduct identifier scope analysis + let module = module.fold_with(&mut resolver(unresolved_mark, top_level_mark, true)); + + // Remove typescript types + let module = module.fold_with(&mut strip(top_level_mark)); + + // Fix up any identifiers with the same name, but different contexts + let module = module.fold_with(&mut hygiene()); + + // Ensure that we have enough parenthesis. + let module = module.fold_with(&mut fixer(Some(&comments))); + + let mut buf = vec![]; + { + let mut emitter = Emitter { + cfg: swc_ecma_codegen::Config { + minify: false, + ..Default::default() + }, + cm: cm.clone(), + comments: Some(&comments), + wr: JsWriter::new(cm.clone(), "\n", &mut buf, None), + }; + + emitter.emit_module(&module).unwrap(); + } + + let diagnostics = diagnostics_cell.borrow(); + ensure_no_fatal_swc_diagnostics(path, &cm, diagnostics.iter())?; + Ok(String::from_utf8(buf).expect("non-utf8?")) + }) +} diff --git a/types/graphics-core.d.ts b/types/graphics-core.d.ts index 41f83ec4..5f79ec8d 100644 --- a/types/graphics-core.d.ts +++ b/types/graphics-core.d.ts @@ -1,3 +1,5 @@ +// These are the functions exposed by the native graphics module. +// export function cls(r: number, g: number, b: number); export function print(msg: string); export function spr( From 82c386fd0f287a12cb6a9a27ccc50c12a9b81ea3 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 24 Jun 2023 23:02:43 -0700 Subject: [PATCH 2/5] [oden-js] Fix bug with repeated arguments --- oden-js/Cargo.lock | 7 + oden-js/Cargo.toml | 5 +- oden-js/src/conversion/function.rs | 292 ++++++++++++++++++++++++++++- 3 files changed, 297 insertions(+), 7 deletions(-) diff --git a/oden-js/Cargo.lock b/oden-js/Cargo.lock index 00f5674c..ad7d556c 100644 --- a/oden-js/Cargo.lock +++ b/oden-js/Cargo.lock @@ -8,6 +8,12 @@ version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "bindgen" version = "0.63.0" @@ -141,6 +147,7 @@ name = "oden-js" version = "0.1.0" dependencies = [ "anyhow", + "assert_matches", "bitflags", "oden-js-sys", "thiserror", diff --git a/oden-js/Cargo.toml b/oden-js/Cargo.toml index cf4bb98a..c9dcd5c5 100644 --- a/oden-js/Cargo.toml +++ b/oden-js/Cargo.toml @@ -9,4 +9,7 @@ edition = "2021" anyhow = "1" bitflags = "1" thiserror = "1" -oden-js-sys = {path = "../oden-js-sys"} \ No newline at end of file +oden-js-sys = {path = "../oden-js-sys"} + +[dev-dependencies] +assert_matches = "1.5.0" diff --git a/oden-js/src/conversion/function.rs b/oden-js/src/conversion/function.rs index cd3e9878..98123db5 100644 --- a/oden-js/src/conversion/function.rs +++ b/oden-js/src/conversion/function.rs @@ -216,7 +216,7 @@ where let vc = C::try_from_value(args[2], &context)?; let vd = D::try_from_value(args[3], &context)?; let ve = E::try_from_value(args[4], &context)?; - let vf = F::try_from_value(args[4], &context)?; + let vf = F::try_from_value(args[5], &context)?; let res = self(context, va, vb, vc, vd, ve, vf); res.into_res(context) } @@ -252,8 +252,8 @@ where let vc = C::try_from_value(args[2], &context)?; let vd = D::try_from_value(args[3], &context)?; let ve = E::try_from_value(args[4], &context)?; - let vf = F::try_from_value(args[4], &context)?; - let vg = G::try_from_value(args[4], &context)?; + let vf = F::try_from_value(args[5], &context)?; + let vg = G::try_from_value(args[6], &context)?; let res = self(context, va, vb, vc, vd, ve, vf, vg); res.into_res(context) } @@ -290,10 +290,290 @@ where let vc = C::try_from_value(args[2], &context)?; let vd = D::try_from_value(args[3], &context)?; let ve = E::try_from_value(args[4], &context)?; - let vf = F::try_from_value(args[4], &context)?; - let vg = G::try_from_value(args[4], &context)?; - let vh = H::try_from_value(args[4], &context)?; + let vf = F::try_from_value(args[5], &context)?; + let vg = G::try_from_value(args[6], &context)?; + let vh = H::try_from_value(args[7], &context)?; let res = self(context, va, vb, vc, vd, ve, vf, vg, vh); res.into_res(context) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Context, Error, EvalFlags, Result, Runtime}; + use assert_matches::assert_matches; + + #[test] + fn no_arguments() { + let rt = Runtime::new(); + let ctx = Context::new(rt); + + let the_function = ctx + .new_fn(|_: &ContextRef| -> Result { Ok(0.0) }) + .unwrap(); + + ctx.global_object() + .unwrap() + .set_property(&ctx, "the_function", &the_function) + .expect("Should be able to set the function"); + + let val = ctx + .eval("the_function()", "script", EvalFlags::NONE) + .unwrap(); + + assert_eq!(String::from("0"), val.to_string(&ctx).unwrap()); + + let val = ctx.eval("the_function(1)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + } + + #[test] + fn one_argument() { + let rt = Runtime::new(); + let ctx = Context::new(rt); + + let the_function = ctx + .new_fn(|_: &ContextRef, a: f32| -> Result { Ok(a) }) + .unwrap(); + + ctx.global_object() + .unwrap() + .set_property(&ctx, "the_function", &the_function) + .expect("Should be able to set the function"); + + let val = ctx + .eval("the_function(1)", "script", EvalFlags::NONE) + .unwrap(); + + assert_eq!(String::from("1"), val.to_string(&ctx).unwrap()); + + let val = ctx.eval("the_function()", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + + let val = ctx.eval("the_function(1,2)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + } + + #[test] + fn two_arguments() { + let rt = Runtime::new(); + let ctx = Context::new(rt); + + let the_function = ctx + .new_fn(|_: &ContextRef, a: f32, b: f32| -> Result { Ok(a + b) }) + .unwrap(); + + ctx.global_object() + .unwrap() + .set_property(&ctx, "the_function", &the_function) + .expect("Should be able to set the function"); + + let val = ctx + .eval("the_function(1,2)", "script", EvalFlags::NONE) + .unwrap(); + + assert_eq!(String::from("3"), val.to_string(&ctx).unwrap()); + + let val = ctx.eval("the_function(1)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + + let val = ctx.eval("the_function(1,2,3)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + } + + #[test] + fn three_arguments() { + let rt = Runtime::new(); + let ctx = Context::new(rt); + + let the_function = ctx + .new_fn(|_: &ContextRef, a: f32, b: f32, c: f32| -> Result { Ok(a + b + c) }) + .unwrap(); + + ctx.global_object() + .unwrap() + .set_property(&ctx, "the_function", &the_function) + .expect("Should be able to set the function"); + + let val = ctx + .eval("the_function(1,2,3)", "script", EvalFlags::NONE) + .unwrap(); + + assert_eq!(String::from("6"), val.to_string(&ctx).unwrap()); + + let val = ctx.eval("the_function(1,2)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + + let val = ctx.eval("the_function(1,2,3,4)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + } + + #[test] + fn four_arguments() { + let rt = Runtime::new(); + let ctx = Context::new(rt); + + let the_function = ctx + .new_fn( + |_: &ContextRef, a: f32, b: f32, c: f32, d: f32| -> Result { + Ok(a + b + c + d) + }, + ) + .unwrap(); + + ctx.global_object() + .unwrap() + .set_property(&ctx, "the_function", &the_function) + .expect("Should be able to set the function"); + + let val = ctx + .eval("the_function(1,2,3,4)", "script", EvalFlags::NONE) + .unwrap(); + + assert_eq!(String::from("10"), val.to_string(&ctx).unwrap()); + + let val = ctx.eval("the_function(1,2,3)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + + let val = ctx.eval("the_function(1,2,3,4,5)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + } + + #[test] + fn five_arguments() { + let rt = Runtime::new(); + let ctx = Context::new(rt); + + let the_function = ctx + .new_fn( + |_: &ContextRef, a: f32, b: f32, c: f32, d: f32, e: f32| -> Result { + Ok(a + b + c + d + e) + }, + ) + .unwrap(); + + ctx.global_object() + .unwrap() + .set_property(&ctx, "the_function", &the_function) + .expect("Should be able to set the function"); + + let val = ctx + .eval("the_function(1,2,3,4,5)", "script", EvalFlags::NONE) + .unwrap(); + + assert_eq!(String::from("15"), val.to_string(&ctx).unwrap()); + + let val = ctx.eval("the_function(1,2,3,4)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + + let val = ctx.eval("the_function(1,2,3,4,5,6)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + } + + #[test] + fn six_arguments() { + let rt = Runtime::new(); + let ctx = Context::new(rt); + + let the_function = ctx + .new_fn( + |_: &ContextRef, a: f32, b: f32, c: f32, d: f32, e: f32, f: f32| -> Result { + Ok(a + b + c + d + e + f) + }, + ) + .unwrap(); + + ctx.global_object() + .unwrap() + .set_property(&ctx, "the_function", &the_function) + .expect("Should be able to set the function"); + + let val = ctx + .eval("the_function(1,2,3,4,5,6)", "script", EvalFlags::NONE) + .unwrap(); + + assert_eq!(String::from("21"), val.to_string(&ctx).unwrap()); + + let val = ctx.eval("the_function(1,2,3,4,5)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + + let val = ctx.eval("the_function(1,2,3,4,5,6,7)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + } + + #[test] + fn seven_arguments() { + let rt = Runtime::new(); + let ctx = Context::new(rt); + + let the_function = ctx + .new_fn( + |_: &ContextRef, + a: f32, + b: f32, + c: f32, + d: f32, + e: f32, + f: f32, + g: f32| + -> Result { Ok(a + b + c + d + e + f + g) }, + ) + .unwrap(); + + ctx.global_object() + .unwrap() + .set_property(&ctx, "the_function", &the_function) + .expect("Should be able to set the function"); + + let val = ctx + .eval("the_function(1,2,3,4,5,6,7)", "script", EvalFlags::NONE) + .unwrap(); + + assert_eq!(String::from("28"), val.to_string(&ctx).unwrap()); + + let val = ctx.eval("the_function(1,2,3,4,5,6)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + + let val = ctx.eval("the_function(1,2,3,4,5,6,7,8)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + } + + #[test] + fn eight_arguments() { + let rt = Runtime::new(); + let ctx = Context::new(rt); + + let the_function = ctx + .new_fn( + |_: &ContextRef, + a: f32, + b: f32, + c: f32, + d: f32, + e: f32, + f: f32, + g: f32, + h: f32| + -> Result { Ok(a + b + c + d + e + f + g + h) }, + ) + .unwrap(); + + ctx.global_object() + .unwrap() + .set_property(&ctx, "the_function", &the_function) + .expect("Should be able to set the function"); + + let val = ctx + .eval("the_function(1,2,3,4,5,6,7,8)", "script", EvalFlags::NONE) + .unwrap(); + + assert_eq!(String::from("36"), val.to_string(&ctx).unwrap()); + + let val = ctx.eval("the_function(1,2,3,4,5,6,7)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + + let val = ctx.eval("the_function(1,2,3,4,5,6,7,8,9)", "script", EvalFlags::NONE); + assert_matches!(val, Err(Error::Exception(..))); + } +} From 1aa3663ca4639f954ae72c95806a97a5a5550d5b Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 24 Jun 2023 23:03:02 -0700 Subject: [PATCH 3/5] [oden] Derive debug --- src/script/graphics.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/script/graphics.rs b/src/script/graphics.rs index af708126..b3058396 100644 --- a/src/script/graphics.rs +++ b/src/script/graphics.rs @@ -2,14 +2,17 @@ use oden_js::{module, ContextRef, Value, ValueResult}; use std::sync::mpsc::Sender; use std::sync::Arc; +#[derive(Debug)] pub struct PrintCommand { pub text: String, } +#[derive(Debug)] pub struct ClearCommand { pub color: [f64; 4], } +#[derive(Debug)] pub struct SpriteCommand { pub x: f32, pub y: f32, @@ -21,6 +24,7 @@ pub struct SpriteCommand { pub sh: f32, } +#[derive(Debug)] pub enum GraphicsCommand { Clear(ClearCommand), Print(PrintCommand), From e3ae371f53851b9df644b8c3bd1eb4cf92588c17 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sun, 25 Jun 2023 07:53:44 -0700 Subject: [PATCH 4/5] [oden] Main is ts now dummy --- src/script.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/script.rs b/src/script.rs index dc761730..67bdc032 100644 --- a/src/script.rs +++ b/src/script.rs @@ -60,7 +60,7 @@ impl ScriptContext { .expect("Graphics module should load without error"); let module = context - .import_module("./src/main.js", "") + .import_module("./src/main.ts", "") .expect("Unable to load main"); let init = module From 0a36ffdde12335253040e401900def5b19c9f169 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sun, 25 Jun 2023 07:54:25 -0700 Subject: [PATCH 5/5] [oden] Input coordinates are pixel coordinates --- src/graphics.ts | 4 +-- src/lib.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++---- src/main.ts | 2 +- src/shader.wgsl | 61 ++++++++++++++++++++++++++++----------- 4 files changed, 117 insertions(+), 26 deletions(-) diff --git a/src/graphics.ts b/src/graphics.ts index b5c067c6..9758d38a 100644 --- a/src/graphics.ts +++ b/src/graphics.ts @@ -22,7 +22,7 @@ export function spr( sw: number | undefined = undefined, sh: number | undefined = undefined ) { - sw = sw || w; - sh = sh || h; + sw = sw || 1.0; + sh = sh || 1.0; core.spr(x, y, w, h, sx, sy, sw, sh); } diff --git a/src/lib.rs b/src/lib.rs index 6bcceb99..e432dfef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ use bytemuck; +use wgpu::util::DeviceExt; use winit::{ event::*, event_loop::{ControlFlow, EventLoop}, @@ -38,6 +39,20 @@ impl Vertex { } } +#[repr(C)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +struct ScreenUniforms { + resolution: [f32; 2], +} + +impl ScreenUniforms { + fn new(width: u32, height: u32) -> ScreenUniforms { + ScreenUniforms { + resolution: [width as f32, height as f32], + } + } +} + struct State { surface: wgpu::Surface, device: wgpu::Device, @@ -52,6 +67,10 @@ struct State { diffuse_bind_group: wgpu::BindGroup, + screen_uniform: ScreenUniforms, + screen_uniform_buffer: wgpu::Buffer, + screen_uniform_bind_group: wgpu::BindGroup, + // Garbage mouse_x: f64, mouse_y: f64, @@ -171,6 +190,37 @@ impl State { label: Some("diffuse_bind_group"), }); + let screen_uniform = ScreenUniforms::new(size.width, size.height); + let screen_uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Screen Uniform Buffer"), + contents: bytemuck::cast_slice(&[screen_uniform]), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let screen_uniform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + label: Some("screen_bind_group_layout"), + }); + + let screen_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &screen_uniform_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: screen_uniform_buffer.as_entire_binding(), + }], + label: Some("camera_bind_group"), + }); + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("Shader"), source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), @@ -179,7 +229,10 @@ impl State { let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Render Pipeline Layout"), - bind_group_layouts: &[&texture_bind_group_layout], + bind_group_layouts: &[ + &texture_bind_group_layout, + &screen_uniform_bind_group_layout, + ], push_constant_ranges: &[], }); @@ -242,6 +295,9 @@ impl State { vertex_buffer, max_vertices, diffuse_bind_group, + screen_uniform, + screen_uniform_buffer, + screen_uniform_bind_group, mouse_x: 0.0, mouse_y: 0.0, @@ -265,7 +321,14 @@ impl State { false } - fn update(&mut self) {} + fn update(&mut self) { + self.screen_uniform = ScreenUniforms::new(self.size.width, self.size.height); + self.queue.write_buffer( + &self.screen_uniform_buffer, + 0, + bytemuck::cast_slice(&[self.screen_uniform]), + ); + } fn render(&mut self, commands: Vec) -> Result<(), wgpu::SurfaceError> { let output = self.surface.get_current_texture()?; @@ -349,19 +412,19 @@ impl State { }); vertices.push(Vertex { position: [sc.x, sc.y + sc.h, 0.0], - tex_coords: [sc.u, sc.v], + tex_coords: [sc.u, sc.v + sc.sh], }); vertices.push(Vertex { position: [sc.x, sc.y + sc.h, 0.0], - tex_coords: [sc.u, sc.v], + tex_coords: [sc.u, sc.v + sc.sh], }); vertices.push(Vertex { position: [sc.x + sc.w, sc.y, 0.0], - tex_coords: [sc.u, sc.v], + tex_coords: [sc.u + sc.sw, sc.v], }); vertices.push(Vertex { position: [sc.x + sc.w, sc.y + sc.h, 0.0], - tex_coords: [sc.u, sc.v], + tex_coords: [sc.u + sc.sw, sc.v + sc.sh], }); } @@ -375,6 +438,7 @@ impl State { .write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&vertices)); render_pass.set_pipeline(&self.render_pipeline); render_pass.set_bind_group(0, &self.diffuse_bind_group, &[]); + render_pass.set_bind_group(1, &self.screen_uniform_bind_group, &[]); render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); render_pass.draw(0..(vertices.len() as u32), 0..1); } diff --git a/src/main.ts b/src/main.ts index 46ad15b9..0a372f56 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,5 +8,5 @@ export function update() {} export function draw() { cls(0.1, 0.2, 0.3); - spr(0, 0, 0.5, 0.5, 0, 0); + spr(0, 0, 320, 240, 0, 0); } diff --git a/src/shader.wgsl b/src/shader.wgsl index 800e4538..331b4543 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -1,31 +1,58 @@ // Vertex shader +struct ScreenUniform { + resolution : vec2f, +}; +@group(1) @binding(0) // 1. + var screen : ScreenUniform; + struct VertexInput { - @location(0) position: vec3, - @location(1) tex_coords: vec2, + @location(0) position : vec3, @location(1) tex_coords : vec2, }; struct VertexOutput { - @builtin(position) clip_position: vec4, - @location(0) tex_coords: vec2, + @builtin(position) clip_position : vec4, + @location(0) tex_coords : vec2, }; -@vertex -fn vs_main( - model: VertexInput, -) -> VertexOutput { - var out: VertexOutput; - out.tex_coords = model.tex_coords; - out.clip_position = vec4(model.position, 1.0); - return out; +const RES = vec2f(320.0, 240.0); // The logical resolution of the screen. + +@vertex fn vs_main(model : VertexInput)->VertexOutput { + var out : VertexOutput; + out.tex_coords = model.tex_coords; + + let RES_AR = RES.x / RES.y; // The aspect ratio of the logical screen. + + // the actual resolution of the screen. + let screen_ar = screen.resolution.x / screen.resolution.y; + + // Compute the difference in resolution ... correctly? + // + // nudge is the amount to add to the logical resolution so that the pixels + // stay the same size but we respect the aspect ratio of the screen. (So + // there's more of them in either the x or y direction.) + var nudge = vec2f(0.0); + if (screen_ar > RES_AR) { + nudge.x = (RES.y * screen_ar) - RES.x; + } else { + nudge.y = (RES.x / screen_ar) - RES.y; + } + var new_logical_resolution = RES + nudge; + + // Now we can convert the incoming position to clip space, in the new screen. + let in_pos = vec2f(model.position.x, model.position.y); + let centered = in_pos + (nudge / 2.0); + let position = (2.0 * centered / new_logical_resolution) - 1.0; + + out.clip_position = vec4f(position, model.position.z, 1.0); + return out; } // Fragment shader -@group(0) @binding(0) var t_diffuse: texture_2d; -@group(0) @binding(1) var s_diffuse: sampler; +@group(0) @binding(0) var t_diffuse : texture_2d; +@group(0) @binding(1) var s_diffuse : sampler; -@fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return textureSample(t_diffuse, s_diffuse, in.tex_coords); +@fragment fn fs_main(in : VertexOutput)->@location(0) vec4 { + return textureSample(t_diffuse, s_diffuse, in.tex_coords); }