diff --git a/.gitignore b/.gitignore index 5b520a87..bbe5a28b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ lib-c/twr.a lib-c/twrd.a lib-js/ azure/ +examples/lib/out/* +examples/pong/out/* diff --git a/.vscode/launch.json b/.vscode/launch.json index 8111b4ae..fc8df711 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,5 +1,15 @@ { "configurations": [ + { + "type": "firefox", + "request": "launch", + "reAttach": true, + "name": "Launch Examples firefox", + "file": "${workspaceFolder}/examples/index.html", + "firefoxArgs": [ + "--enable-features=SharedArrayBuffers" + ] + }, { "name": "Examples", "type": "chrome", diff --git a/docs/api/api-c-audio.md b/docs/api/api-c-audio.md new file mode 100644 index 00000000..6ff1a261 --- /dev/null +++ b/docs/api/api-c-audio.md @@ -0,0 +1,129 @@ +--- +title: Audio API for WebAssembly +description: twr-wasm provides an audio C API that allows Wasm code to call a subset of the JS Audio API. +--- + +# Audio API for WebAssembly + +This section describes twr-wasm's C Audio API, which allows audio API functions to be called using C/C++ from WebAssembly. + +## Examples +| Name | View Live Link | Source Link | +| - | - | - +| Pong (C++) | [View Pong](/examples/dist/pong/index.html) | [Source for Pong](https://github.com/twiddlingbits/twr-wasm/tree/main/examples/pong) | +| tests-audio | [View tests-audio](/examples/dist/tests-audio/index.html) | [Source for tests-audio](https://github.com/twiddlingbits/twr-wasm/tree/main/examples/tests-audio) | + +## Code Example +~~~c title="Play Audio" +#include "twr-audio.h" +#include +#include + +#define M_PI 3.14159265358979323846 + +void play() { + twr_audio_play_file("example.mp3"); //plays audio from specified URL + + const long SAMPLE_RATE = 48000; //48,000 samples per second + const double DURATION = 10.0; //play for 10 seconds + const double freq = 493.883; //Middle B (B4) + + long length = (long)ceil(SAMPLE_RATE*DURATION); + //PCM audio data in the form of -1.0 to 1.0 + float* wave = (float*)malloc(sizeof(float) * length); + + //generate square wave at specified frequency and duration + for (long i = 0; i < length; i++) { + wave[i] = cos(2*M_PI*freq*(i/(float)sample_rate)) > 0 ? 1 : -1; + } + + //creates a mon-audio channel buffer at our given SAMPLE_RATE + // and square-wave data we generated + long node_id = twr_audio_from_float_pcm(1, SAMPLE_RATE, wave, length); + + //plays the square wave + // Can be played multiple times, and is only freed on twr_audio_free + twr_audio_play(node_id); +} +~~~ + +## Overview +The Audio API is part a twr-wasm library that can be accessed via `#include "twr-audio.h"`. It has two main methods to play audio: from raw PCM data and from a URL. + +Raw PCM data can be initialized via `twr_audio_from__pcm` or `twr_audio_load`. There are multiple types for `twr_audio_from__pcm`. These types include Float, 8bit, 16bit, and 32bit. Float takes in values between -1.0 and 1.0. Meanwhile, the 8bit, 16bit, and 32bit versions take them in as signed numbers between their minimum and maximum values. `twr_audio_load`, on the other hand, reads the PCM data from an audio file that is specified by a URL. This method does not stream the audio, so it might take some time to read the file (depending how long the file is). Each function returns an integer `node_id` that identifies the loaded PCM data. Once the PCM data is initialized, it can be played via functions like `twr_audio_play`, `twr_audio_play_range`, `twr_audio_play_sync`, and `twr_audio_play_range_sync`. The play functions can be called multiple times for each audio ID. + +You can also play audio directly from a URL. Unlike `twr_audio_load`, the url is initialized directly into an `HTMLAudioElement` which streams the audio and starts playback immediately. + +In addition to playing audio, there are functions that allow you to query and modify an ongoing playback. These include stopping the playback, getting how long it's been playing, and modifying the pan; volume; or playback rate of the audio. + +## Notes +When playing audio, a `playback_id` is returned to query or modify the playback. However, once playback completes, the `playback_id` becomes invalid. Most functions that take in a `playback_id` will simply return a warning and return without error if the `playback_id` is invalid. An exception is `twr_audio_query_playback_position` which will return -1 when given an invalid `playback_id`. + +Functions that end in `_sync` are for use with `twrWamModuleAsync`, and are synchronous. Meaning they don't return until the operation, such as playback, is complete. + +Note that on some platforms, sounds that are too short might not play correctly. This is true in JavaScript as well. + +## Functions +These are the current Audio APIs available in C/C++: + +~~~c +long twr_audio_from_float_pcm(long num_channels, long sample_rate, float* data, long singleChannelDataLen); +long twr_audio_from_8bit_pcm(long number_channels, long sample_rate, char* data, long singleChannelDataLen); +long twr_audio_from_16bit_pcm(long number_channels, long sample_rate, short* data, long singleChannelDataLen); +long twr_audio_from_32bit_pcm(long number_channels, long sample_rate, int* data, long singleChannelDataLen); + +float* twr_audio_get_float_pcm(long node_id, long* singleChannelDataLenPtr, long* numChannelsPtr); +char* twr_audio_get_8bit_pcm(long node_id, long* singleChannelDataLenPtr, long* numChannelsPtr); +short* twr_audio_get_16bit_pcm(long node_id, long* singleChannelDataLenPtr, long* numChannelsPtr); +int* twr_audio_get_32bit_pcm(long node_id, long* singleChannelDataLenPtr, long* numChannelsPtr); + +long twr_audio_play(long node_id); +long twr_audio_play_volume(long node_id, double volume, double pan); +long twr_audio_play_callback(long node_id, double volume, double pan, int finish_callback); + +struct PlayRangeFields { + double pan, volume; + int loop, finish_callback; + long sample_rate; +}; +struct PlayRangeFields twr_audio_default_play_range(); +long twr_audio_play_range(long node_id, long start_sample, long end_sample); +long twr_audio_play_range_ex(long node_id, long start_sample, long end_sample, struct PlayRangeFields* fields); + +long twr_audio_play_sync(long node_id); +long twr_audio_play_sync_ex(long node_id, double volume, double pan); + + +struct PlayRangeSyncFields { + double pan, volume; + int loop; + long sample_rate; +}; + +struct PlayRangeSyncFields twr_audio_default_play_range_sync(); +long twr_audio_play_range_sync(long node_id, long start_sample, long end_sample); +long twr_audio_play_range_sync_ex(long node_id, long start_sample, long end_sample, struct PlayRangeSyncFields* fields); + +long twr_audio_load_sync(char* url); +long twr_audio_load(int event_id, char* url); +long twr_audio_query_playback_position(long playback_id); +void twr_audio_free_id(long node_id); + +void twr_audio_stop_playback(long playback_id); + +void twr_audio_modify_playback_volume(long playback_id, double volume); +void twr_audio_modify_playback_pan(long playback_id, double pan); +void twr_audio_modify_playback_rate(long playback_id, double sample_rate); + +long twr_audio_play_file(char* file_url); +long twr_audio_play_file_ex(char* file_url, double volume, double playback_rate, int loop); + +struct AudioMetadata { + long length; + long sample_rate; + long channels; +}; + +void twr_audio_get_metadata(long node_id, struct AudioMetadata* metadata); +~~~ + diff --git a/docs/api/api-c-con.md b/docs/api/api-c-con.md index e6d995de..36556f20 100644 --- a/docs/api/api-c-con.md +++ b/docs/api/api-c-con.md @@ -38,7 +38,7 @@ from ``: ### twr_get_console This function will retrieve a console by its name. The standard names are `stdio`, `stderr`, and `std2d`. In addition, any named console that was passed to a module using the `io` option can be retrieved with this function. -See [io doc](../api/api-typescript.md#io-option-multiple-consoles-with-names). +See [io doc](../api/api-ts-modules.md#io-option-multiple-consoles-with-names). See the [multi-io example](../examples/examples-multi-io.md). @@ -155,7 +155,7 @@ int io_get_height(twr_ioconsole_t* io); ### io_set_colors For addressable display consoles only. -Sets a 24 bit RGB default color for the foreground and background. The prior default colors are changed (lost). For example, if you set the default colors when you created the console (see [twrConsoleTerminal Options](../api/api-typescript.md#class-twrconsoleterminal)), the defaults will no longer be active. Use `io_get_colors` to save existing colors for later restoration using `io_set_colors`. +Sets a 24 bit RGB default color for the foreground and background. The prior default colors are changed (lost). For example, if you set the default colors when you created the console (see [twrConsoleTerminal Options](../api/api-ts-consoles.md#class-twrconsoleterminal)), the defaults will no longer be active. Use `io_get_colors` to save existing colors for later restoration using `io_set_colors`. A call to `io_set_colors` doesn't actually cause any on screen changes. Instead, these new default colors are used in future draw and text calls. A foreground and background color is set for each cell in the console window. The cell's colors are set to these default foreground/background colors when a call to `io_setc`, `io_setreset`, etc is made. @@ -253,7 +253,7 @@ Gets a string from a Console. Returns when the user presses "Enter". Displays This function is commonly used with [`stdin`.](../api/api-c-con.md#getting-a-console) -This function requires that you use [`twrWasmModuleAsync`.](../api/api-typescript.md#class-twrwasmmoduleasync) +This function requires that you use [`twrWasmModuleAsync`.](../api/api-ts-modules.md#class-twrwasmmoduleasync) ~~~c #include @@ -275,7 +275,7 @@ bool io_point(twr_ioconsole_t* io, int x, int y); ### io_putc Sends a byte to an IoConsole and supports the current locale's character encoding. This function will "stream" using the current code page. In other words, if you `io_putc` ASCII, it will work as "normal". If the current locale is set to 1252, then you can send windows-1252 encoded characters. If the current locale is UTF-8, then you can stream UTF-8 (that is, call `io_putc` once for each byte of the multi-byte UTF-8 character). -Note that when characters are sent to the browser console using `stderr` they will not render to the console until a newline, return, or ASCII 03 (End-of-Text) is sent. +Note that when characters are sent to the browser console using `stderr` they will not render to the console until a newline or return is sent. ~~~c #include "twr-io.h" @@ -349,7 +349,7 @@ void io_end_draw(twr_ioconsole_t* io); This function has been removed. Use `stderr` or `twr_conlog`. ~~~c -#include "twr-wasm.h" +#include "twr-crt.h" twr_conlog("hello 99 in hex: %x", 99); ~~~ diff --git a/docs/api/api-c-d2d.md b/docs/api/api-c-d2d.md index 81b6f229..b8db87e4 100644 --- a/docs/api/api-c-d2d.md +++ b/docs/api/api-c-d2d.md @@ -34,7 +34,7 @@ void square() { ## Overview The Draw 2D APIs are C APIs and are part of the twr-wasm library that you access with `#include "twr-draw2d.h"`. There is also a C++ canvas wrapper class in `examples/twr-cpp` used by the balls and pong examples. -To create a canvas surface, that you can draw to using the twr-wasm 2D C drawing APIs, you can use the `twrConsoleCanvas` class in your JavaScript/HTML ([see Consoles Section](../gettingstarted/stdio.md)). Or more simply, if you add a canvas tag to your HTML named `twr_d2dcanvas`, the needed `twrConsoleCanvas` will be created automatically. +To create a canvas surface, that you can draw to using the twr-wasm 2D C drawing APIs, use the `twrConsoleCanvas` class in your JavaScript/HTML ([see Consoles Section](../gettingstarted/stdio.md)). Or more simply, if you add a canvas tag to your HTML named `twr_d2dcanvas`, the needed `twrConsoleCanvas` will be created automatically. ~~~js @@ -48,7 +48,7 @@ To draw using the C 2D Draw API: - call `d2d_end_draw_sequence` - repeat as desired -`d2d_start_draw_sequence` will draw to the default `twrConsoleCanvas`, as explained at the start of this section. `d2d_start_draw_sequence_with_con` is optional, and allows you to specify the `twrConsoleCanvas` to draw to. You would typically get this console in C using the `twr_get_console` function ([which retrieves a named console](../api/api-typescript.md#io-option-multiple-consoles-with-names) that you specified in the `io` module option.) +`d2d_start_draw_sequence` will draw to the default `twrConsoleCanvas`, as explained at the start of this section. `d2d_start_draw_sequence_with_con` is optional, and allows you to specify the `twrConsoleCanvas` to draw to. You would typically get this console in C using the `twr_get_console` function ([which retrieves a named console](../api/api-ts-modules.md#io-option-multiple-consoles-with-names) that you specified in the `io` module option.) Commands are queued until flushed -- which will take the batch of queued draw commands, and execute them. The 2D draw APIs will work with either `twrWasmModule` or `twrWasmModuleAsync`. With `twrWasmModuleAsync`, the batch of commands is sent from the worker thread over to the JavaScript main thread for execution. By batching the calls between calls to `d2d_start_draw_sequence` and `d2d_end_draw_sequence`, performance is improved. @@ -66,7 +66,7 @@ Some commands have extra details that you need to be aware of to avoid performan * getLineDash takes in a buffer_length, double * array (the buffer), and returns the amount of the buffer filled. If there are more line segments than can fit in the buffer_length, a warning is printed and the excess is voided. If you want to know the size before hand for allocation, the getLineDashLength function is available. ## Notes -The functions listed below are based on the JavaScript Canvas 2D API ([found here](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D)). However, there are some slight differences since these APIS are made for C rather than JavaScript. For example some items keep resources stored on the JavaScript side (such as d2d_createlineargradient) which are referenced by a numeric ID , rather than an actual object reference. +The functions listed below are based on the JavaScript Canvas 2D API ([found here](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D)). However, there are some slight differences since these APIs are made for C rather than JavaScript. For example some items keep resources stored on the JavaScript side (such as d2d_createlineargradient) which are referenced by a numeric ID , rather than an actual object reference. Additionally, there are alternative functions like d2d_setstrokestylergba, which calls the same underlying function as d2d_setstrokestyle, but takes in a color as a number rather than CSS style string. @@ -74,7 +74,53 @@ As noted above, putImageData requires that the image data be valid until flush i Other functions that take a string, like d2d_filltext, don't have this same issue because they make a copy of the string argument. These string copies will be automatically freed. -d2d_load_image should be called outside of a d2d_start_draw_sequence segment. If you are loading it to an id that you plan on freeing, ensure that the buffer is flushed before doing so as d2d_load_image bypasses it. In addition, d2d_load_image requires you to be using twrWasmAsyncModule as it waits for the image to load (or fail) before returning. +getCanvasPropDouble, getCanvasPropString, setCanvasPropDouble, and setCanvasPropString allow you to change canvas properties by name. If the previous values type is either undefined, a string rather than a number, etc. then it will throw an error so ensure that you have your property names correct. + +d2d_load_image is not called like other instructions which rely on d2d_start_draw_sequence. This means it always gets called immediately and doesn't queue up in or flush the instruction queue. This can cause some issues such as the example below. +~~~c title="Load Image Pitfall" +#include "twr-draw2d.h" +bool has_background = false; +const long BACKGROUND_ID = 1; +//draws the background +void draw_background(struct d2d_draw_seq* ds) { + assert(has_background); + d2d_drawimage(ds, BACKGROUND_ID, x, y); +} +//loads a new background image +void load_background_image(struct d2d_draw_seq* ds, const char * url) { + if (has_background) { + //free previous background + //this isn't called until the buffer in ds get's flushed. + // For this program, that doesn't happen until d2d_end_draw_sequence is called, + // so d2d_load_image processes before d2d_releasid throws a warning and then is deleted when d2d_releaseid + // is eventually called. + d2d_releaseid(ds, BACKGROUND_ID); + //d2d_flush(ds) //by adding a flush like so, it ensures releaseid is called before d2d_load_image + } else { + has_background = true; + } + d2d_load_image(url, BACKGROUND_ID); +} +void render() { + struct d2d_draw_seq* ds=d2d_start_draw_sequence(100); + + //load background + load_background_image(ds, "example_image.com"); + + draw_background(ds); //draw it + + d2d_end_draw_sequence(ds); + + + struct d2d_draw_seq* ds=d2d_start_draw_sequence(100); + + //load new background image + load_background_image(ds, "example_image2.com"); + draw_background(ds); + + d2d_end_draw_sequence(ds); +} +~~~ ## Functions These are the Canvas APIs currently available in C: @@ -104,6 +150,9 @@ void d2d_setfillstyle(struct d2d_draw_seq* ds, const char* css_color); void d2d_setfont(struct d2d_draw_seq* ds, const char* font); void d2d_setlinecap(struct d2d_draw_seq* ds, const char* line_cap); void d2d_setlinejoin(struct d2d_draw_seq* ds, const char* line_join); +void d2d_setlinedash(struct d2d_draw_seq* ds, unsigned long len, const double* segments); +unsigned long d2d_getlinedash(struct d2d_draw_seq* ds, unsigned long length, double* buffer); +unsigned long d2d_getlinedashlength(struct d2d_draw_seq* ds); void d2d_setlinedashoffset(struct d2d_draw_seq* ds, double line_dash_offset); void d2d_createlineargradient(struct d2d_draw_seq* ds, long id, double x0, double y0, double x1, double y1); @@ -126,7 +175,10 @@ void d2d_quadraticcurveto(struct d2d_draw_seq* ds, double cpx, double cpy, doubl void d2d_rect(struct d2d_draw_seq* ds, double x, double y, double width, double height); void d2d_closepath(struct d2d_draw_seq* ds); +//deprecated, use d2d_ctoimagedata instead void d2d_imagedata(struct d2d_draw_seq* ds, long id, void* mem, unsigned long length, unsigned long width, unsigned long height); + +void d2d_ctoimagedata(struct d2d_draw_seq* ds, long id, void* mem, unsigned long length, unsigned long width, unsigned long height); void d2d_putimagedata(struct d2d_draw_seq* ds, long id, unsigned long dx, unsigned long dy); void d2d_putimagedatadirty(struct d2d_draw_seq* ds, long id, unsigned long dx, unsigned long dy, unsigned long dirtyX, unsigned long dirtyY, unsigned long dirtyWidth, unsigned long dirtyHeight); @@ -141,15 +193,19 @@ void d2d_settransformmatrix(struct d2d_draw_seq* ds, const struct d2d_2d_matrix void d2d_transform(struct d2d_draw_seq* ds, double a, double b, double c, double d, double e, double f); void d2d_transformmatrix(struct d2d_draw_seq* ds, const struct d2d_2d_matrix * transform); void d2d_resettransform(struct d2d_draw_seq* ds); -void d2d_setlinedash(struct d2d_draw_seq* ds, unsigned long len, const double* segments); -unsigned long d2d_getlinedash(struct d2d_draw_seq* ds, unsigned long length, double* buffer); -unsigned long d2d_getlinedashlength(struct d2d_draw_seq* ds); bool d2d_load_image(const char* url, long id); bool d2d_load_image_with_con(const char* url, long id, twr_ioconsole_t * con); void d2d_drawimage(struct d2d_draw_seq* ds, long id, double dx, double dy); -void d2d_getimagedata(struct d2d_draw_seq* ds, double x, double y, double width, double height, void* buffer, unsigned long buffer_len); +void d2d_drawimage_ex(struct d2d_draw_seq* ds, long id, double sx, double sy, double sWidth, double sHeight, double dx, double dy, double dWidth, double dHeight); +void d2d_getimagedata(struct d2d_draw_seq* ds, long id, double x, double y, double width, double height); unsigned long d2d_getimagedatasize(double width, double height); +void d2d_imagedatatoc(struct d2d_draw_seq* ds, long id, void* buffer, unsigned long buffer_len); + +double d2d_getcanvaspropdouble(struct d2d_draw_seq* ds, const char* prop_name); +void d2d_getcanvaspropstring(struct d2d_draw_seq* ds, const char* prop_name, char* buffer, unsigned long buffer_len); +void d2d_setcanvaspropdouble(struct d2d_draw_seq* ds, const char* prop_name, double val); +void d2d_setcanvaspropstring(struct d2d_draw_seq* ds, const char* prop_name, const char* val); ~~~ d2d_measuretext() returns this structure: diff --git a/docs/api/api-c-general.md b/docs/api/api-c-general.md index ea7d5b1f..b63939a1 100644 --- a/docs/api/api-c-general.md +++ b/docs/api/api-c-general.md @@ -107,7 +107,7 @@ The current implementation does not wait for the debug string to output to the c ## twr_epoch_timems Returns the number of milliseconds since the start of the epoch. ~~~ -#include "twr-wasm.h" +#include "twr-crt.h" uint64_t twr_epoch_timems(); ~~~ @@ -197,7 +197,7 @@ char* twr_mbgets(char* buffer); Internally this function uses the [stdio](../gettingstarted/stdio.md) IoConsole -- see the IoConsole section for more advanced input/output. -This function will encode characters as specified by the LC_CTYPE category of the current locale. ASCII is used for "C", and UTF-8 and Windows-1252 are also supported (see [localization](../api/api-localization.md)) +This function will encode characters as specified by the LC_CTYPE category of the current locale. ASCII is used for "C", and UTF-8 and Windows-1252 are also supported (see [localization](../api/api-c-localization.md)) Note that C character input is blocking and you must use twrWasmModuleAsync -- see [stdin](../gettingstarted/stdio.md) for details on how to enable blocking character input. @@ -213,15 +213,63 @@ size_t twr_mbslen_l(const char *str, locale_t locale); `twr_sleep` is a traditional blocking sleep function. This function is blocking, and so is only available if you use `twrWasmModuleAsync`. ~~~ -#include "twr-wasm.h" +#include "twr-crt.h" void twr_sleep(int ms); ~~~ +## twr_register_callback +Returns a new event ID that is paired with the specified C function. This event ID can be passed to functions that accept an event ID. When the event is triggered, the specified callback is called. + +The callback function's first argument will be the event ID. Subsequent arguments are event specific. It is legal to register the same callback for multiple event IDs. + +~~~c +#include "twr-crt.h" + +int twr_register_callback(const char* func_name); +~~~ + +For example: +~~~c +// timer event callback (called once) +__attribute__((export_name("on_timer1"))) +void on_timer1(int event_id) { + printf("timer callback 1 entered (event id=%d) !\n", event_id); +} + +// entry point +__attribute__((export_name("twr_main"))) +void twr_main() { + int timer1=twr_register_callback("on_timer1"); + twr_timer_single_shot(2000, timer1); +} +~~~ + +## twr_timer_single_shot +Triggers the specified event (callback) once after `milliSeconds`. Returns a `timerID` which can be used with `twr_timer_cancel`. + +~~~ +int twr_timer_single_shot(int milliSeconds, int eventID); +~~~ + +## twr_timer_repeat +Triggers the specified event (callback) repeatedly after `milliSeconds`. Returns a `timerID` which can be used with `twr_timer_cancel`. + +~~~ +int twr_timer_repeat(int milliSeconds, int eventID); +~~~ + +## twr_timer_cancel +Cancels the specfied timer. + +~~~ +void twr_timer_cancel(int timerID); +~~~ + ## twr_tofixed This function is identical to its JavaScript version. ~~~ -#include "twr-wasm.h" +#include "twr-crt.h" void twr_tofixed(char* buffer, int buffer_size, double value, int dec_digits); ~~~ @@ -232,7 +280,7 @@ The functions to convert double to text are `snprintf`, `fcvt_s`,`twr_dtoa`, `tw This function is identical to its JavaScript version. ~~~ -#include "twr-wasm.h" +#include "twr-crt.h" void twr_toexponential(char* buffer, int buffer_size, double value, int dec_digits); ~~~ diff --git a/docs/api/api-localization.md b/docs/api/api-c-localization.md similarity index 100% rename from docs/api/api-localization.md rename to docs/api/api-c-localization.md diff --git a/docs/api/api-c-stdlib.md b/docs/api/api-c-stdlib.md index 68e54b09..d043e9be 100644 --- a/docs/api/api-c-stdlib.md +++ b/docs/api/api-c-stdlib.md @@ -56,6 +56,8 @@ void srand(int seed); #define __min(a,b) (((a) < (b)) ? (a) : (b)) #define __max(a,b) (((a) > (b)) ? (a) : (b)) +int abs(int n); + int _fcvt_s( char* buffer, size_t sizeInBytes, @@ -103,10 +105,10 @@ void assert(int expression); ## math.h ~~~ -int abs(int n); double acos(double arg); double asin(double arg); double atan(double arg); +double atan2(double y, double x); double ceil(double arg); double cos(double arg); double exp(double arg); diff --git a/docs/api/api-ts-consoles.md b/docs/api/api-ts-consoles.md new file mode 100644 index 00000000..eaf4608f --- /dev/null +++ b/docs/api/api-ts-consoles.md @@ -0,0 +1,154 @@ +--- +title: TypeScript-JavaScript API to create i/o Consoles +description: twr-wasm provides TypeScript/JavaScript classes to create I/O Consoles. +--- + +# Console Classes +This section describes the twr-wasm TypeScript/JavaScript classes that you use to create I/O Consoles for character streaming, a terminal, or 2D Canvas Drawing + +The classes `twrConsoleDiv`, `twrConsoleTerminal`, `twrConsoleDebug`, and `twrConsoleCanvas` create consoles that enable user i/o. Your C/C++ can direct user interactive i/o to these consoles. + +## Related Console Documentation +- [Console Introduction](../gettingstarted/stdio.md) +- [Console C APIs](../api/api-c-con.md) + +## class twrConsoleDebug +`twrConsoleDebug` streamings characters to the browser debug console. + +C type: `IO_TYPE_CHARWRITE` + +There are no constructor parameters. + +## class twrConsoleDiv +`twrConsoleDiv` streams character input and output to a div tag . + +C type: `IO_TYPE_CHARREAD` and `IO_TYPE_CHARWRITE` + +The div tag will expand as you add more text (via printf, etc). + +You pass a `
` element to use to render the Console to to the `twrConsoleDiv` constructor. For example: +~~~js +
+ + + +~~~ + +There are constructor options to set the color and font size. You can also set these directly in the HTML for your `
` tag. If you wish to change the default font, set the font in the `div` tag with the normal HTML tag options. + +~~~js title="twrConsoleDiv constructor options" +constructor(element:HTMLDivElement, params:IConsoleDivParams) + +export interface IConsoleDivParams { + foreColor?: string, + backColor?: string, + fontSize?: number, +} +~~~ + +You can use the `putStr` member function to print a string to the div console in JavaScript. + +## class twrConsoleTerminal +`twrConsoleTerminal` provides streaming and addressable character input and output. A `` tag is used to render into. + +C types: `IO_TYPE_CHARREAD`, `IO_TYPE_CHARWRITE`, `IO_TYPE_ADDRESSABLE_DISPLAY` + +twrConsoleTerminal is a simple windowed terminal and supports the same streamed output and input features as a does `twrConsoleDiv`, but also supports x,y coordinates, colors, and other features. The window console supports chunky (low res) graphics (each character cell can be used as a 2x3 graphic array). + +The canvas width and height, in pixels, will be set based on your selected font size and the width and height (in characters) of the terminal. These are passed as constructor options when you instantiate the `twrConsoleTerminal`. + +You can use the `putStr` member function on twrConsoleTerminal to print a string to the terminal in JavaScript. + +As you add more text (via printf, etc), the `twrConsoleTerminal` will scroll if it becomes full (unlike `twrConsoleDiv`, which expands) + +[A list of C functions](../api/api-c-con.md#io-console-functions) that operate on `twrConsoleTerminal` are available. + +Here is an example: +~~~js + + + + + + +~~~ + +~~~js title="twrConsoleTerminal constructor options" +constructor (canvasElement:HTMLCanvasElement, params:IConsoleTerminalParams) + +// see twrConsoleDiv options elsewhere, which are also supported +export interface IConsoleTerminalParams extends IConsoleDivParams { + widthInChars?: number, + heightInChars?: number, +} +~~~ + +## class twrConsoleCanvas +`twrConsoleCanvas` creates a 2D drawing surface that the Canvas compatible [2d drawing APIs](../api/api-c-d2d.md) can be used with. + +C type: `IO_TYPE_CANVAS2D`. + +~~~js +constructor(element:HTMLCanvasElement) +~~~ + +~~~js title="twrConsoleCanvas Example" + + canvas id="canvas1for2d"> + + +~~~ + diff --git a/docs/api/api-ts-library.md b/docs/api/api-ts-library.md new file mode 100644 index 00000000..1b0a4be4 --- /dev/null +++ b/docs/api/api-ts-library.md @@ -0,0 +1,369 @@ +--- +title: Use twrLibrary to Implement Wasm C/C++ API in TypeScript +description: twr-wasm allows you to implement new C/C++ APIs in JavaScript/TypeScript by extending the twrLibrary class. +--- + +# twr-wasm Libraries +twr-wasm Libraries are used to expose TypeScript code to C/C++ as C APIs. All of the twr-wasm C APIs are implemented with twr-wasm libraries. You can also use a library to implement your own C APIs using TypeScript. + +There are two kinds of Libraries: + +- Those that have only once instance (such as the math library) +- Those that can have multiple instances across one more more library types, where each library type implements the same interface. Consoles are an example of this (see [interfaceName](#interfacename)). + +## Basic Steps +twr-wasm Libraries support both `twrWasmModule` and `twrWasmModuleAsync`. That is, when you create a twrLibrary, it will function with either type of module. In many cases no extra work is needed for the `twrWasmModuleAsync`, but in some cases, extra code is needed. + +The class `twrLibrary` provides the core functionality: + + - Support for functions to be imported into the WebAssembly Module + - Support for `twrWasmModuleAsync` proxy Web Worker thread + - An event framework, allowing you to post events to WebAssembly C code. + +To implement a twr-wasm library you: + +- create a new TypeScript class that extends `class twrLibrary`. +- create a C .h file for your new functions (with function signatures) +- add one or more functions to your TypeScript class +- add the functions to the `import` object (that is part of your class) +- consider if special handling is needed for `twrWasmModuleAsync` (more on this below) + +## Lib Example +See the `lib` [example here](../examples/examples-lib.md) for a more complete example which shows how each of the different use cases can be handled. In addition, you can look in `/source/twr-ts` for files that start with `twrlib*` or `twrcon*` for examples. + +## Example twrLibTimer +The following code is from the twr-wasm source for twrlibtimer. + +- `twr_timer_single_shot` - sends an event to C after the timer times out. +- `twr_sleep` - blocks C execution for a period of time. + +~~~js +import {IWasmModule,} from "./twrmod.js" +import {IWasmModuleAsync} from "./twrmodasync.js" +import {twrLibrary, TLibImports, twrLibraryInstanceRegistry} from "./twrlibrary.js" + +// Libraries use default export +export default class twrLibTimer extends twrLibrary { + id: number; + imports:TLibImports = { + twr_timer_single_shot:{}, + twr_sleep:{isAsyncFunction: true, isModuleAsyncOnly: true}, + }; + + libSourcePath = new URL(import.meta.url).pathname; + + constructor() { + // all library constructors should start with these two lines + super(); + this.id=twrLibraryInstanceRegistry.register(this); + } + + twr_timer_single_shot(callingMod:IWasmModule|IWasmModuleAsync, milliSeconds:number, eventID:number) { + setTimeout(()=>{ + callingMod.postEvent(eventID) + }, milliSeconds); + } + + async twr_sleep_async(callingMod:IWasmModuleAsync, milliSeconds:number) { + const p = new Promise( (resolve)=>{ + setTimeout(()=>{ resolve() }, milliSeconds); + }); + + return p; + } + +} +~~~ + +## C Header Files +You need to create a .h file that provides signatures to the C users of your new API. For example, for this library your .h file would be this: + +~~~ +__attribute__((import_name("twr_timer_single_shot"))) void twr_timer_single_shot(int ms, int event_id); +__attribute__((import_name("twr_sleep"))) void twr_sleep(int ms); +~~~ + +The purpose of `import_name` code is to export your functions from WebAssembly to JavaScript. These are an equivalent alternative to adding the functions to an `wasm-ld` `-export` option. + +## Registering your API +To register you class so that the APIs are available to C code, you use code akin to this in your `index.html` (or similar): +~~~ +import twrLibTimerMod from "./twrlibtimer.js" // default export + +new twrLibTimerMod(); // will register itself +~~~ + +If you are a contributor to twr-wasm and plan to add your library as new built-in APIs, add the registration to `twrLibBultins.ts` + +## Example Function Explained +Here is what is happening in this code: + +~~~ +imports:TLibImports = { + twr_timer_single_shot:{}, +} + +// this function will work in both twrWasmModule and twrWasmModuleAsync +twr_timer_single_shot(callingMod:IWasmModule|IWasmModuleAsync, milliSeconds:number, eventID:number) { + setTimeout(()=>{ + callingMod.postEvent(eventID) + }, milliSeconds); +} +~~~ + +`twr_timer_single_shot` is listed in the `imports` class member variable which causes the function `twr_timer_single_shot` to be added to the WebAssembly.ModuleImports imports. + +The argument for `callingMod:IWasmModule|IWasmModuleAsync` is filled in by the `twrLibrary` code -- the calling C function does not include this as an argument. All Parameters following `callingMod` are passed by the C code calling the function. + +`twr_timer_single_shot` creates a JavaScript timer. When the timer completes, an event is posted, which will trigger a callback in the C code. + + +## Events +To receive an Event, the C code needs to register an event callback, and then pass the event ID to the function that will generate events. Like this: + +~~~c +__attribute__((export_name("on_timer"))) +void on_timer(int event_id) { + printf("timer callback 2 entered (event id=%d)\n", event_id); +} + +__attribute__((export_name("twr_main"))) +void twr_main() { + int timer1=twr_register_callback("on_timer"); + twr_timer_single_shot(2000, timer); +~~~ + +In this example, `on_timer` will be called after 2 seconds. + +`__attribute__((export_name("on_timer")))` is needed because this C function is exported out of the WebAssembly module and into the JavaScript code. JavaScript needs to call this function. + +In this example, the event does not have any arguments. But it may -- integers (which includes pointers) can be accepted as arguments to the event callback. These arguments are event specific. + +## imports +All TypeScript functions that you wish to import into a WebAssembly module as C APIs, should be listed in the `imports` object. + +Each function in the `imports` object has optional options, that are primarily for use with `twrWasmModuleAsync` modules. + +`imports` are added to WebAssembly.ModuleImports imports. + +## callingMod +Each function listed in the `import` section will be passed a module as the first parameter. In general, a function should be written to handle being called either with `IWasmModule` or `IWasmModuleAsync` as the calling module interface ( `callingMod:IWasmModule|IWasmModuleAsync` ). This is generally straight forward. + +Examples that might cause some extra work, and that are covered below, are: + +- Implementing blocking functions like `twr_sleep` +- Allocating memory, for example, to return a string + +## Numbers Only +All of the parameters received by an `import` function need to be numbers. These functions interface directly with the WebAssembly module with no conversion. If you are passing or returning strings, or accessing structures, you will need to use the data access functions that are provided in `callingMod.memWasm` (more on this below). The general issue and approach is [explained in this document.](../gettingstarted/parameters.md). + +## memWasm +A `callingMod` member function that you may need to use is `memWasm` (`callingMod.memWasm`). `memWasm` is used to access data in the WebAssembly Memory. This will happen when you need to dereference a pointer, access strings, or access structures. [See `wasmMem` documentation here](api-ts-memory.md#accessing-data-in-webassembly-memory). + +`memWasm` is exposed by both `IWasmModule` and `IWasmModuleAsync`. + +- `IWasmModule` exposes `wasmMem: IWasmMemory` +- `IWasmModuleAsync` exposes `wasmMem: IWasmMemoryAsync`. + +If you wish to write a function that accesses the `async` `PutXXX` functions, you should use the `isAsyncFunction: true` option. + +## `twrWasmModule` and `twrWasmModuleAsync`. +twrLibrary's are designed to work with either `twrWasmModule` and `twrWasmModuleAsync`. (Recall that twr-wasm has two different module types: `twrWasmModule` and `twrWasmModuleAsync`). + +- `twrWasmModule` runs in the JavaScript main thread, and thus all functions that it exposes are asynchronous -- meaning that they should return relatively quickly and not block execution. +- `twrWasmModuleAsync` runs in a JavaScript Worker thread , and thus all functions that it exposes are synchronous -- meaning they can block C execution. The "Async" in `twrWasmModuleAsync` refers to the fact that javaScript can `await` on `twrWasmModuleAsync` blocking APIs. It takes blocking APIs and makes them "asynchronous" to the JavaScript main thread. + +Although many functions can be listed in `imports` and written without worrying about which type of module is using them, this isn't always true. Some tomes extra code or thought is needed to have optimal APIs for `twrWasmModuleAsync`. + +In the above example, `twr_timer_single_shot` will work correctly with both `twrWasmModule` and `twrWasmModuleAsync`. It is an asynchronous function -- meaning that it returns quickly. + +However, the function `twr_sleep` blocks C code, and will only work with `twrWasmModuleAsync`. + +## `twrWasmModuleAsync` thread structure +To understand more clearly why `twrWasmModuleAsync` might need more attention, it is helpful to understand how it is allocates task between its two threads: the JavaScript main thread, and a worker thread. + +### The `twr_sleep` function is used to illustrate thread structure +The function `twr_sleep_async` will only function with `twrWasmModuleAsync` because it causes the C code to block. For example, `twr_sleep` can be used like this: + +~~~c +printf("going to sleep..."); +twr_sleep(1000); +printf("awake!\n"); +~~~ +### `twrWasmModuleAsync` uses Two Threads +The`twrWasmModuleAsync` consists of two threads: The JavaScript main thread, and the Web Worker. By default the code for your library functions is always executed in the JavaScript main thread. In the Web Worker, an internal class called `twrWasmModuleAsyncProxy` executes. The default execution (unless `isCommonCode` is specified -- which is explained later), happens like this: + +1. in C: twr_sleep() is called +2. in `twrWasmModuleAsyncProxy`, a message is sent to the JavaScript main thread, requesting execution of the `twrLibTimer.twr_sleep` function. +3. `twrWasmModuleAsyncProxy` is paused (thus `twr_sleep` is blocking), waiting for a response to the message sent ins step 2. +3. The JavaScript main thread receives the message, and `awaits` on `twrLibTimer.twr_sleep` +4. When the Promise that is being awaited on resolves, the JavaScript main threads sends a reply back to `twrWasmModuleAsyncProxy` indicating that execution is complete. If there are any return codes they are also sent (twr_sleep does not have a return code) +5. `twr_sleep` returns to the C caller + + +The above sequence actually happens for all `import` functions by default when using `twrWasmModuleAsync`, irregardless if or how long they block for. This is because certain JavaScript code can only execute in the JavaScript main thread. Import function options exists to modify this behavior, in the cases where it is not desired. + +The above steps also glosses over an important point -- the method that the `twrWasmModuleAsyncProxy` uses to wait for a response from the main JavaScript thread. In step 3 above (Worker thread is blocking from `twr_sleep` call), the worker thread is blocking on a call to `Atomics.wait`. Communication from the JavaScript main thread to the Worker is through shared memory and a circular buffer. This is how twrWasmModuleAsync is able to block execution of the C code. This means that the Worker Thead main event loop can block for long periods of time -- perhaps indefinitely. And this means common JavaScript code can not run reliably in the Worker thread. For example, a setTimeout callback may not happen (because it is dispatched by the main JavaScript event loop). Likewise, Animations won't work since they are often executed inside the JavaScript event loop. This is another important reason that all the `import` code generally runs inside the JavaScript main thread. + +## Blocking Function Explained + +twr-wasm supports blocking functions like sleep when the API user is using `twrWasmModuleAsync`. This section explains the `sleep` function which causes C code to block. In other words, in C, code like this will work: + +~~~c +printf("going to sleep..."); +twr_sleep(1000); +printf("awake!\n"); +~~~ + +The TypeScript twrLibrary derived class implementation looks like this: + +~~~js +// this function will only work in twrWasmModuleAsync since it blocks the C caller. +async twr_sleep_async(callingMod:IWasmModuleAsync, milliSeconds:number) { + const p = new Promise( (resolve)=>{ + setTimeout(()=>{ resolve() }, milliSeconds); + }); + + return p; +} +~~~ + +And has these import options set: +~~~js +imports:TLibImports = { + twr_sleep:{isAsyncFunction: true, isModuleAsyncOnly: true}, +}; +~~~ + +`isAsyncFunction: true` is telling twrLibrary to call the `twr_sleep_async` function with `await` and so allow the function being called to `await`. +`isModuleAsyncOnly: true` is telling twrLibrary that this function only exists when `twrWasmModuleAsync` is used. + +This code will execute in the JavaScript main thread. The Calling C code (`twr_sleep`) will block in a Worker thread while waiting for this code in the JavaScrit main thread to complete. The function `twr_sleep_async` creates a JavaScript promise that the calling code will `await` on. Once the promise resolves, the calling function will unblock the C `twr_sleep` function that is in the Worker Thread. + +## `import` options +The various `import` options are used to handle different cases for `twrWasmModuleAsync`. + +The import options are: +~~~ +isAsyncFunction?:boolean; +isModuleAsyncOnly?:boolean; +isCommonCode?:boolean; +~~~ + + +### `isAsyncFunction` +This option is used when you wish to `await` inside the implementation of your function. This option also specifies that the function will be called with `await`. + +This option will only modify behavior this way when your function is called from `twrWasmModuleAsync`. + +- If this option is not specified, the same `import` function will be used for both module types, and the function can not use the `await` keyword. +- if this option is specified, then when `twrWasmModuleAsync` calls the indicated `import` function, it will call a version of the function that has `_async` append to the function name. This means that you will create two versions of the `import` `funcA` - `funcA` and `funcA_async`. +- If, however, you also specify the option `isModuleAsyncOnly`, then only the `_async` function is expected. + +Here is an example of how declare a function with the `import` option `isAsyncFunction`: + +~~~ +async twr_sleep_async(callingMod:IWasmModuleAsync, milliSeconds:number) +~~~ + +Note that: + +- the function declaration starts with the async keyword +- the function has the suffix `_async` appended to its `import` name +- that the function is passed an `IWasmModuleAsync` as the callingMod. + +By using this option, you may need to create two versions of the `import` function -- one that is `async` (for use by `twrWasmModuleAsync`), and one that does not use the `async` keyword--for use by `twrWasmModule`. + +In a case like `twr_sleep`, there is only one function implemented for sleep - the async function. But this is not always the case. For example, if your `import` function uses the `wasmMem.PutXX` functions, you will need to create two functions for the `import`. Here is an example: + +~~~js + imports:TLibImports = { + ex_append_two_strings:{isAsyncFunction: true}, + }; + + ex_append_two_strings(callingMod:IWasmModule, str1Idx:number, str2Idx:number) { + const newStr=callingMod.wasmMem.getString(str1Idx)+callingMod.wasmMem.getString(str2Idx); + const rv=callingMod.wasmMem.putString(newStr); + return rv; + } + + async ex_append_two_strings_async(callingMod:IWasmModuleAsync, str1Idx:number, str2Idx:number) { + const newStr=callingMod.wasmMem.getString(str1Idx)+callingMod.wasmMem.getString(str2Idx); + const rv=await callingMod.wasmMem.putString(newStr); + return rv; + } +~~~ + +### `isModuleAsyncOnly` +This option specifies that the indicated function is only available to `twrWasmModuleAsync`. This option should be used for functions that block C execution. The `twr_sleep` is an example. + +### `isCommonCode` +This option is used to specify a function that should be used directly by the `twrWasmModuleAsync` Web Worker. Without this option, the behavior is that code running in the Web Worker will send a message to the JavaScript Main thread requesting that the function be executed in the context of the JavaScript main thread. The Web Worker will then wait for a reply to the message before continuing C execution. However, in certain cases, it is possible, and might be more performant, to have the code execute directly in the WorkerThread. + +There are limitations on the code that will work correctly with `isCommonCode`: + +- The functions must be available to a Worker thread +- The function can not be `async` (that is, it can not use `await`) +- The function can not call `PostEvent` +- The functions must not depend on the Worker's main event loop running (this event loop often doesn't execute with the `twrWasmModuleAsync` Worker thread.) + - The function can not use a callback (the callback won't get called because callbacks are often dispatched in the JavaScript main event loop) + +Here is an Example of using `isCommonCode`: + +~~~js +export default class twrLibMath extends twrLibrary { + imports:TLibImports = { + twrSin:{isCommonCode: true}, + } + + libSourcePath = new URL(import.meta.url).pathname; + + twrSin(callingMod:IWasmModule|twrWasmBase, angle:number ) {return Math.sin(angle)} +} +~~~ + +In this case the `Math.sin` function is available in both a Web Worker and the JavaScript main thread. It is a simple function, that works fine without the JavaScript event loop operating. + +### noBlock +`noBlock` will cause a function call in an `twrWasmModuleAsync` to send the message from the Worker proxy thread to the JS main thread to execute the function, but not to wait for the result. This should only be used for functions with a `void` return value. This has the advantage that (a) the C code will not block waiting for a void return value (so it returns faster), and (b) it takes advantage of multiple cores by allowing the JS Main thread and the Worker thread to execute in parallel. + +Note that the messages sent from the proxy thread to the JS main thread (for function execution) will cause execution of function calls to serialize, and so if a function that blocks (waits for results from JS main thread) is called after a call with `noBlock`, everything should work as expected. + +Do not use `noBlock` if: + + - the function returns a value + - the C code should not continue executing until the function completes execution. + - if the following scenario could arise: + - funcA (with noBlock) called + - funcB called and returns a value or otherwise depends on funcA completing execution, and funcA uses async keyword. + +Use `noBlock` carefully. + +## libSourcePath + Always set this as follows: + ~~~js + libSourcePath = new URL(import.meta.url).pathname; + ~~~ + + `libSourcePath` is used to uniquely identify the library class, as well as to dynamically import the library when `isCommonCode` is used. + +## interfaceName +In a twrLibrary, + + - An "interface" refers to the set of functions that the library exposes to C. Ie, the functions in the `import` object. + - The name of the interface is anonymous, unless `interfaceName` is set. + - An undefined interfaceName (anonymous interface) means that only one instance of that class is allowed (for example `twrLibMath`) + - Set `interfaceName` to a unique name when multiple instances that support the same interface are allowed (for example the twr-wasm Consoles). + - Multiple classes may have the same interfaceName (a class is identified by its libSourcePath). For example `twrConDiv`, `twrConDebug`, `twrConTerminal` all have the same interface. + +When multiple instances of classes with the same interface are enabled (by setting `interfaceName`), the first argument in every C function call is expected to be the twrLibrary `id` (a member variable of the twrLibrary derived class). The twrLibrary will use this `id` to route the function call to the correct instance of the library. The `id` is not passed to the twrLibrary function (even though it is required to be the first C arg). + +The twrLibrary instance should be created in the JavaScript main thread, and passed to the module in the [`io` option.](api-ts-modules.md#io-option-multiple-consoles-with-names) The C code can discover the `id`, by using the [`twr_get_console`.](api-c-con.md#twr_get_console) + +~~~js title='example' +interfaceName = "twrConsole"; +~~~ + +## The `twrWasmModuleAsync` Event Loop +TODO + diff --git a/docs/api/api-ts-memory.md b/docs/api/api-ts-memory.md new file mode 100644 index 00000000..fe016737 --- /dev/null +++ b/docs/api/api-ts-memory.md @@ -0,0 +1,68 @@ +--- +title: TypeScript-JavaScript API to access data in WebAssembly memory +description: twr-wasm provides TypeScript/JavaScript classes to read and write integers, doubles, strings, and more from WebAssembly Memory +--- + +# Accessing Data in WebAssembly Memory +There are situations where you may need to access WebAssembly memory from your TypeScript code. For example, if you need to allocate a new string, de-reference a pointer, or examine or modify a structure. A [`TwrLibrary`](../api/api-ts-library.md) in particular may need to do this. ( For background on the issues involved in using WebAssembly Memory with C and TypeScript see [Passing Function Arguments from JavaScript to C/C++ with WebAssembly](../gettingstarted/parameters.md).). + + To access WebAssembly memory you will use the `wasmMem` public member variable: + +- `twrWasmModule` has the public member variable `wasmMem:IWasmMemory` +- `twrWasmModuleAsync` has the public member variable `wasmMem:IWasmMemoryAsync` + +If you are writing a [`twrLibrary`](./api-ts-library.md), the appropriate `wasmMem` is the first parameter of your import functions. + +Both versions of wasmMem extend `IWasmMemoryBase` which has common functions for retrieving or setting values from WebAssembly memory. With `IWasmMemoryAsync`, for functions that call `malloc` internally, `await` must be used. This situation arises in the `IWasmMemoryAsync` versions of the `PutXXX` functions -- they return a Promise. `PutXX` makes a call to `malloc`, and in `twrWasmModuleAsync`, `malloc` needs to message the Worker thread and `await` for a response. + +`mem8`, `mem32`, `memF`, and `memD` can be used to access the WebAssembly memory directly. They are different views on the same memory. + +`getU8Arr` and `getU32Arr` expect that `idx` (a pointer) points to a: +~~~ +struct { + int size; + void* data; +} +~~~ + +Note: In prior versions of twr-wasm, these functions were available directly on the module instance. For example, `mod.GetString`. These functions have been deprecated. Now you should use `mod.wasmMem.getString` (for example). + +~~~js +// IWasmMemoryBase operate on shared memory, so they will function in any WasmModule +export interface IWasmMemoryBase { + memory:WebAssembly.Memory; + mem8:Uint8Array; + mem32:Uint32Array; + memF:Float32Array; + memD:Float64Array; + stringToU8(sin:string, codePage?:number):Uint8Array; + copyString(buffer:number, buffer_size:number, sin:string, codePage?:number):void; + getLong(idx:number): number; + setLong(idx:number, value:number):void; + getDouble(idx:number): number; + setDouble(idx:number, value:number):void; + getShort(idx:number): number; + getString(strIndex:number, len?:number, codePage?:number): string; + getU8Arr(idx:number): Uint8Array; + getU32Arr(idx:number): Uint32Array; +} + +// IWasmMemory does not support await, and so will only work in a thread that has the module loaded +// That would be twrWasmModule, twrWasmModuleAsyncProxy +export interface IWasmMemory extends IWasmMemoryBase { + malloc:(size:number)=>number; + free:(size:number)=>void; + putString(sin:string, codePage?:number):number; + putU8(u8a:Uint8Array):number; + putArrayBuffer(ab:ArrayBuffer):number; +} + +// IWasmMemoryAsync must be used from an async function since await is needed +export interface IWasmMemoryAsync extends IWasmMemoryBase { + malloc:(size:number)=>Promise; + free:(size:number)=>Promise; + putString(sin:string, codePage?:number):Promise; + putU8(u8a:Uint8Array):Promise; + putArrayBuffer(ab:ArrayBuffer):Promise; +} +~~~ diff --git a/docs/api/api-typescript.md b/docs/api/api-ts-modules.md similarity index 52% rename from docs/api/api-typescript.md rename to docs/api/api-ts-modules.md index 432c289c..151aed20 100644 --- a/docs/api/api-typescript.md +++ b/docs/api/api-ts-modules.md @@ -1,32 +1,58 @@ --- -title: TypeScript-JavaScript API to load & call Wasm, Consoles -description: twr-wasm provides TypeScript/JavaScript classes to load Wasm modules, call C, create I/O Consoles. Use Blocking or non-blocking C code. +title: TypeScript-JavaScript API to load & call Wasm +description: twr-wasm provides TypeScript/JavaScript classes to load Wasm modules and call C. Use Blocking or non-blocking C code. --- -# TypeScript-JavaScript API
Load and call Wasm, Create i/o Consoles -This section describes the twr-wasm TypeScript/JavaScript classes that you use to: +# Wasm Modules +This section describes the twr-wasm TypeScript/JavaScript classes `twrWasmModule` and `twrWasmModuleAsync` that are used to load `.wasm` modules, call their C functions, and access wasm memory. Both classes have similar APIs. -- load your Wasm modules, and to call C functions in your Wasm modules. -- create I/O Consoles for character streaming, a terminal, or 2D Canvas Drawing +## About `twrWasmModule` +`class twrWasmModule` allows you to integrate WebAssembly C/C++ code into your Web Page. You can call C/C++ functions, and read and write WebAssembly memory. Function calls are asynchronous, as is normal for a JavaScript function. That is C/C++ functions should not block - they should return quickly (just as happens in JavaScript). -`class twrWasmModule` and `class twrWasmModuleAsync` are used to load .wasm modules and call their C functions. Both classes have similar APIs. The primary difference is that `class twrWasmModuleAsync` proxies functionality through a Web Worker thread, which allows blocking C functions to be called in your WebAssembly Module. The `Async` part of `twrWasmModuleAsync` refers to the ability to `await` on a blocking `callC` in your JavaScript main thread, when using `twrWasmModuleAsync`. +The constructor accepts an optional object (type `IModOpts`), which is explained further down. +~~~js +import {twrWasmModule} from "twr-wasm"; -The classes `twrConsoleDiv`, `twrConsoleTerminal`, `twrConsoleDebug`, and `twrConsoleCanvas` create consoles that enable user i/o. Your C/C++ can direct user interactive i/o to these consoles. See [Console Introduction](../gettingstarted/stdio.md) for information on enabling character input and output in a module. +const mod = new twrWasmModule(); +~~~ -## APIs Common to twrWasmModule and twrWasmModuleAsync -### Common Constructor Options -See [module options below](#module-options). +## About `twrWasmModuleAsync` +`class twrWasmModuleAsync` allows you to integrate WebAssembly C/C++ code into your Web Page that uses a CLI pattern or code that blocks. For example, with `twrWasmModuleAsync` your C/C++ code can call a synchronous function for keyboard input (that blocks until the user has entered the keyboard input). Or your C/C++ code can `sleep` or otherwise block. This is the pattern that is used by many standard C library functions - `fread`, etc. -### loadWasm -Use `loadWasm` to load your compiled C/C++ code (the `.wasm` file). +`class twrWasmModuleAsync` creates a WorkerThread that runs in parallel to the JavaScript main thread. This Worker thread executes your C/C++ code, and proxies functionality that needs to execute in the JavaScript main thread via remote procedure calls. This allows the JavaScript main thread to `await` on a blocking `callC` in your JavaScript main thread. + +The `Async` part of the `twrWasmModuleAsync` name refers to the property of `twrWasmModuleAsync` that makes your synchronous C/C++ code asynchronous. + +The APIs in `class twrWasmModuleAsync` are identical to `class twrWasmModule`, except that certain functions use the `async` keyword and thus need to be called with `await`. This happens whenever the function needs to cross the JavaScript main thread and the Worker thread boundary. For example: `callC` or `malloc`. + +The constructor accepts an optional object (type `IModOpts`), which is explained further down. + +~~~js +import {twrWasmModuleAsync} from "twr-wasm"; + +const amod = new twrWasmModuleAsync(); ~~~ + + +## loadWasm +This function is available on both `class twrWasmModule` and `class twrWasmModuleAsync`. + +Use `loadWasm` to load your compiled C/C++ code (the `.wasm` file). +~~~js await mod.loadWasm("./mycode.wasm") ~~~ -### callC +## callC +This function is available on both `class twrWasmModule` and `class twrWasmModuleAsync`. `twrWasmModuleAsync` returns a Promise, `twrWasmModule` does not. + After your .`wasm` module is loaded with `loadWasm`, you call functions in your C/C++ from TypeScript/JavaScript like this: + +~~~js title='twrWasmModule' +let result=mod.callC(["function_name", ...params]) ~~~ -let result=await mod.callC(["function_name", param1, param2]) + +~~~js title='twrWasmModuleAsync' +let result=await mod.callC(["function_name", ...params]) ~~~ If you are calling into C++, you need to use `extern "C"` like this in your C++ function: @@ -57,31 +83,36 @@ Fo more details, see the [Compiler Options](../gettingstarted/compiler-opts.md). - `bigint` - will be converted into an `int64_t` or equivalent - `string` - converted to a `char *` of malloc'd module memory where string is copied into - `ArrayBuffer` - the array is copied into malloc'd module memory. If you need to pass the length, pass it as a separate argument. Any modifications to the memory made by your C code will be reflected back into the JavaScript ArrayBuffer. - - `URL` - the url contents are copied into malloc'd module Memory, and two C arguments are generated - index (pointer) to the memory, and length -`callC` returns the value returned by the C function. `long`, `int32_t`, `int`, `float` or `double` and the like are returned as a `number`. `int64_t` is returned as a `bigint`, and pointers are returned as a `number`. The contents of the pointer will need to be extracted using the [functions listed below](#accessing-data-in-the-webassembly-memory). More details can be found in this article: [Passing Function Arguments to WebAssembly](../gettingstarted/parameters.md) and [in this example](../examples/examples-callc.md). The [FFT example](../examples/examples-fft.md) demonstrates passing and modifying a `Float32Array` view of an `ArrayBuffer`. +`callC` returns the value returned by the C function. `long`, `int32_t`, `int`, `float` or `double` and the like are returned as a `number`. `int64_t` is returned as a `bigint`, and pointers are returned as a `number`. The contents of the pointer will need to be extracted using the [functions listed below](api-ts-memory.md). More details can be found in this article: [Passing Function Arguments to WebAssembly](../gettingstarted/parameters.md) and [in this example](../examples/examples-callc.md). The [FFT example](../examples/examples-fft.md) demonstrates passing and modifying a `Float32Array` view of an `ArrayBuffer`. -## class twrWasmModule -This class is used when your C function call will not block (that is, they will not take 'a long time' to execute). +Although you can always use `await` on a `callC`, it is only strictly necessary if the module is of class `twrWasmModuleAsync`. -The constructor accepts an optional object (type `IModOpts`), which is explained further down. -~~~ -import {twrWasmModule} from "twr-wasm"; +`CallC` is mapped to `wasmCall.callC`. `wasmCall` also exposes `callCImpl`, which can be used if no argument conversion is needed.That is if the [arguments are all numbers).](../gettingstarted/parameters.md#webassembly-virtual-machine-intrinsic-capabilities) -const mod = new twrWasmModule(); +## fetchAndPutURL +`fetchAndPutURL` is available as a member function of both `twrWasmModule` and `twrWasmModuleAsync`. + +~~~js +fetchAndPutURL(fnin:URL) : Promise<[number, number]>; ~~~ -## class twrWasmModuleAsync -This class is used to enable blocking C functions, suchs as `sleep` or traditional C style blocking input (such as `getc`); +The returned array contains the index of where the URL contents have been loaded into wasm memory (in index 0), and the length (in index 1). -The constructor accepts an optional object (type `IModOpts`), which is explained further down. +In prior versions of twr-wasm, `callC` could accept an argument type of URL. This is no longer supported, and instead `fetchAndPutURL` should be used. +## log +`log` is available as a member function of both `twrWasmModule` and `twrWasmModuleAsync`. + +`log` is similar to the JavaScript `console.log`, except that the output is sent to the `stdio` console. + +~~~js +log(...params: string[]):void; ~~~ -import {twrWasmModuleAsync} from "twr-wasm"; - -const amod = new twrWasmModuleAsync(); -~~~ +Also note that most consoles have a `putStr` function. + +## twrWasmModuleAsync Details `twrWasmModuleAsync` implements all of the same functions as `twrWasmModule`, plus allows blocking inputs, and blocking code generally. This is achieved by proxying all the calls through a Web Worker thread. For example, with this C function in your Wasm module: @@ -233,171 +264,5 @@ Note: - `forecolor` and `backcolor` - if stdio is set to `twrConsoleDiv` or `twrConsoleTerminal`, these can be set to a CSS color (like '#FFFFFF' or 'white') to change the default background and foreground colors. However, these are deprecated, and instead, use the `twrConsoleDiv` or `twrConsoleTerminal` constructor options. - `fonsize` - Changes the default fontsize if stdio is set to `twrConsoleDiv` or `twrConsoleTerminal`. Deprecated, instead use `twrConsoleDiv` or `twrConsoleTerminal` constructor options. - `TStdioVals` have been removed (they were a not too useful option in prior versions of twr-wasm) -- `divLog` is deprecated. Instead use the `putStr` member function on most consoles. - -## Console Classes - -### class twrConsoleDebug -`twrConsoleDebug` streamings characters to the browser debug console. - -C type: `IO_TYPE_CHARWRITE` - -There are no constructor parameters. - -### class twrConsoleDiv -`twrConsoleDiv` streams character input and output to a div tag . - -C type: `IO_TYPE_CHARREAD` and `IO_TYPE_CHARWRITE` - -The div tag will expand as you add more text (via printf, etc). - -You pass a `
` element to use to render the Console to to the `twrConsoleDiv` constructor. For example: -~~~js -
- - - -~~~ - -There are constructor options to set the color and font size. You can also set these directly in the HTML for your `
` tag. If you wish to change the default font, set the font in the `div` tag with the normal HTML tag options. - -~~~js title="twrConsoleDiv constructor options" -constructor(element:HTMLDivElement, params:IConsoleDivParams) - -export interface IConsoleDivParams { - foreColor?: string, - backColor?: string, - fontSize?: number, -} -~~~ - -### class twrConsoleTerminal -`twrConsoleTerminal` provides streaming and addressable character input and output. A `` tag is used to render into. - -C types: `IO_TYPE_CHARREAD`, `IO_TYPE_CHARWRITE`, `IO_TYPE_ADDRESSABLE_DISPLAY` - -twrConsoleTerminal is a simple windowed terminal and supports the same streamed output and input features as a does `twrConsoleDiv`, but also supports x,y coordinates, colors, and other features. The window console supports chunky (low res) graphics (each character cell can be used as a 2x3 graphic array). - -The canvas width and height, in pixels, will be set based on your selected font size and the width and height (in characters) of the terminal. These are passed as constructor options when you instantiate the `twrConsoleTerminal`. - -You can use the `putStr` member function on most consoles to print a string to the terminal in JavaScript. - -As you add more text (via printf, etc), the `twrConsoleTerminal` will scroll if it becomes full (unlike `twrConsoleDiv`, which expands) - -[A list of C functions](../api/api-c-con.md#io-console-functions) that operate on `twrConsoleTerminal` are available. - -Here is an example: -~~~js - - - - - - -~~~ - -~~~js title="twrConsoleTerminal constructor options" -constructor (canvasElement:HTMLCanvasElement, params:IConsoleTerminalParams) - -// see twrConsoleDiv options elsewhere, which are also supported -export interface IConsoleTerminalParams extends IConsoleDivParams { - widthInChars?: number, - heightInChars?: number, -} -~~~ - -### class twrConsoleCanvas -`twrConsoleCanvas` creates a 2D drawing surface that the Canvas compatible [2d drawing APIs](../api/api-c-d2d.md) can be used with. - -C type: `IO_TYPE_CANVAS2D`. - -~~~js -constructor(element:HTMLCanvasElement) -~~~ - -~~~js title="twrConsoleCanvas Example" - - canvas id="canvas1for2d"> - - -~~~ - -## Accessing Data in the WebAssembly Memory -`callC()` will convert your JavaScript arguments into a form suitable for use by your C code. However, if you return or want to access struct values inside TypeScript you will find the following `twrWasmModule` and `twrWasmModuleAsync` functions handy. See the [callc example](../examples/examples-callc.md) and [Passing Function Arguments from JavaScript to C/C++ with WebAssembly](../gettingstarted/parameters.md) for an explanation of how these functions work. -~~~js -async putString(sin:string, codePage=codePageUTF8) // returns index into WebAssembly.Memory -async putU8(u8a:Uint8Array) // returns index into WebAssembly.Memory -async putArrayBuffer(ab:ArrayBuffer) // returns index into WebAssembly.Memory -async fetchAndPutURL(fnin:URL) // returns index into WebAssembly.Memory -async malloc(size:number) // returns index in WebAssembly.Memory. - -stringToU8(sin:string, codePage=codePageUTF8) -copyString(buffer:number, buffer_size:number, sin:string, codePage=codePageUTF8):void -getLong(idx:number): number -setLong(idx:number, value:number) -getDouble(idx:number): number -setDouble(idx:number, value:number) -getShort(idx:number): number -getString(strIndex:number, len?:number, codePage=codePageUTF8): string -getU8Arr(idx:number): Uint8Array -getU32Arr(idx:number): Uint32Array - -memory?:WebAssembly.Memory; -mem8:Uint8Array; -mem32:Uint32Array; -memD:Float64Array; -~~~ +- `divLog` has been renamed `log`. Or use the `putStr` member function on most consoles. diff --git a/docs/examples/examples-lib.md b/docs/examples/examples-lib.md new file mode 100644 index 00000000..58d0ffcf --- /dev/null +++ b/docs/examples/examples-lib.md @@ -0,0 +1,21 @@ +--- +title: lib - Example of adding TypeScript APIs to C/C++ +description: This example demos how to implement code in TypeScript that can be called by your twr-wasm C/C++ code. Uses class twrLibrary. +--- + +# class twrLibrary Example +## What It Does +This example is a twr-wasm Library that implements functions that can be called by your C/C++ code. A twr-wasm Library is written in TypeScript, and derives from the class twrLibrary. + +The lib example demos: + +* Creating functions in TypeScript that can be called from C/C++ +* Posting Events to C +* Implementing blocking as well as non-blocking functions + +## Running Examples and Source: + +- [View lib output](/examples/dist/lib/index.html) +- [Source for lib](https://github.com/twiddlingbits/twr-wasm/tree/main/examples/lib) + +Also see [twr-wasm Libraries Documentation](./../api/api-ts-library.md) diff --git a/docs/examples/examples-overview.md b/docs/examples/examples-overview.md index 1cb38bbd..b6332603 100644 --- a/docs/examples/examples-overview.md +++ b/docs/examples/examples-overview.md @@ -23,22 +23,28 @@ These examples are a good place to learn how to configure clang and wasm-ld to c ## Console Examples | Name | Description | Link | | -----| ----------- | ---- | -| divcon | This simple C program demos inputting and
printing characters to a `div` tag | [divcon](examples-divcon.md) | -| terminal |This simple C program demos writing and inputting
from a `` tag that twr-wasm configures
as a windowed "terminal" | [terminal](examples-terminal.md)| -| multi-io | Demo 6 simultaneous consoles: stream i/o, terminal, 2D Drawing | [multi-io](examples-multi-io.md)| +| divcon | A simple C program demos inputting and printing characters to a `div` tag | [divcon](examples-divcon.md) | +| terminal |A simple C program demos writing and inputting from a `` tag
that twr-wasm configures as a windowed "terminal" | [terminal](examples-terminal.md)| +| multi-io | Demo 6 simultaneous consoles: stream i/o, terminal, and 2D Drawing. | [multi-io](examples-multi-io.md)| ## Draw 2D Examples | Name | Description | Link | | -----| ----------- | ---- | -| balls | These fun Bouncing Balls are written in C++ and demo the
2D drawing APIs with a C++ Canvas wrapper class | [balls](examples-balls.md) | -| pong | A simple game of Pong written in C++ to demo 2D drawing APIs with a C++ canvas wrapper class and taking user input from JS | [pong](examples-pong.md) -| maze | This is an old Win32 program ported to wasm
and demos the 2D Draw APIs | [maze](examples-maze.md) | +| balls | These fun Bouncing Balls are written in C++ and demo the 2D drawing
APIs with a C++ Canvas wrapper class | [balls](examples-balls.md) | +| pong | A simple game of Pong written in C++ to demo 2D drawing APIs with a
C++ canvas wrapper class and taking user input from JS | [pong](examples-pong.md) +| maze | This is an old Win32 program ported to wasm and demos 2D Draw APIs | [maze](examples-maze.md) | ## Call Argument Examples | Name | Description | Link | | -----| ----------- | ---- | -| callC | A demo of passing and returning values between
JavaScript and Wasm module | [callc](examples-callc.md) | -| fft | A demo of calling a C library to perform an FFT
that is graphed in TypeScript | [fft](examples-fft.md) | +| callC | A demo of passing and returning values between JavaScript and Wasm module | [callc](examples-callc.md) | +| fft | A demo of calling a C library to perform an FFT that is graphed in TypeScript | [fft](examples-fft.md) | + +## twrLibrary examples +| Name | Description | Link | +| -----| ----------- | ---- | +| lib | A demo of createing a twrLibrary (use TypeScript to create C/C++ APIs) | [library](examples-lib.md) | + ### Unit Tests @@ -47,6 +53,8 @@ These examples are a good place to learn how to configure clang and wasm-ld to c | tests | twr-wasm unit tests | [tests](/examples/dist/tests/index.html) | | tests-user | "cli" for tests using libc++ and `` | [tests-user](/examples/dist/tests-user/index.html) | | tests-libcxx | Smoke test for libc++. Shows how to use libc++. | [tests-libcxx](examples-libcxx.md) | +| tests-d2d | Unit tests for Draw 2D canvas console | [tests-d2d](examples-tests-d2d.md) | +| tests-audio | Unit tests for the Audio Library | [tests-audio](examples-tests-audio.md) | ## Running or Building the examples locally Online versions of the examples [can be viewed here.](https://twiddlingbits.dev/examples/dist/index.html) diff --git a/docs/examples/examples-pong.md b/docs/examples/examples-pong.md index 4b72aa0b..0f43fad3 100644 --- a/docs/examples/examples-pong.md +++ b/docs/examples/examples-pong.md @@ -1,10 +1,10 @@ --- title: Pong - Game written in C++ using WebAssembly -description: A 2D drawing WebAssembly C/C++ example of singleplayer Pong using Canvas like 2D API with twr-wasm +description: A 2D drawing WebAssembly C/C++ example Pong using Canvas like 2D API with twr-wasm --- # Pong - 2D Game Example -Similar to the [balls example](examples-balls.md), this example uses twr-wasm's 2D Draw API and a C++ canvas class to run a simple game of singleplayer Pong. +Similar to the [balls example](examples-balls.md), this example uses twr-wasm's 2D Draw API and a C++ canvas class to implement a simple game of 2 player and single player Pong with WebAssembly. * [View Pong](/examples/dist/pong/index.html) * [Source for Pong](https://github.com/twiddlingbits/twr-wasm/tree/main/examples/pong) @@ -14,9 +14,14 @@ The Pong example demonstrates * C++ * Using twr-wasm draw 2D APIs that match Javascript Canvas APIs. * Using the twr-wasm canvas.cpp class. -* Taking in Javascript events as user input +* A custom typescript library +* User mouse and keyboard input via events +* Using the Audio Library to play simple sounds This example does not use libc++, which results in smaller code size. For an example that uses libc++ see [tests-libcxx](examples-libcxx.md). ## Screen Grab of Pong Example - + + + + diff --git a/docs/examples/examples-tests-audio.md b/docs/examples/examples-tests-audio.md new file mode 100644 index 00000000..c1081dcb --- /dev/null +++ b/docs/examples/examples-tests-audio.md @@ -0,0 +1,14 @@ +--- +title: Audio API WebAssembly Unit Tests +description: Simple unit tests of the various Audio functions +--- + +# tests-audio - Unit tests for Audio Library +This is a simple set of Unit Tests for testing the Audio API functions. It includes a test for each Audio function. WARNING: Some of the audio played can be loud, so turn down your volume before playing. + +- [view tests-audio example running live](/examples/dist/tests-audio/index.html) +- [View tests-audio source code](https://github.com/twiddlingbits/twr-wasm/tree/main/examples/tests-audio) + +Also see these WebAssembly programs that use this API + +- [Pong](examples-pong.md) diff --git a/docs/examples/examples-tests-d2d.md b/docs/examples/examples-tests-d2d.md new file mode 100644 index 00000000..0d43dfac --- /dev/null +++ b/docs/examples/examples-tests-d2d.md @@ -0,0 +1,15 @@ +--- +title: D2D canvas API WebAssembly Unit Tests +description: Simple unit tests of the various d2d functions +--- + +# tests-d2d - Unit tests for Draw 2D canvas console +This is a simple set of Unit Tests for testing the D2D API functions. It includes a test for each D2D function as well as a test for memory leaks within the API itself. + +- [view tests-d2d example running live](/examples/dist/tests-d2d/index.html) +- [View tests-d2d source code](https://github.com/twiddlingbits/twr-wasm/tree/main/examples/tests-d2d) + +Also see these WebAssembly programs that use this API + +- [Balls](examples-balls.md) +- [Pong](examples-pong.md) diff --git a/docs/gettingstarted/basicsteps.md b/docs/gettingstarted/basicsteps.md index b6167a52..8a9ae2bd 100644 --- a/docs/gettingstarted/basicsteps.md +++ b/docs/gettingstarted/basicsteps.md @@ -8,33 +8,29 @@ This section describes the basic steps to integrate your TypeScript/JavaScript w ## Overview of WebAssembly Project -Your C/C++ WebAssembly project will consist of HTML (and related JavaScript or TypeScript) and C or C++ source files that are compiled into a "`.wasm`" binary file that is loaded as a WebAssembly module by your JavaScript. +Your C/C++ WebAssembly project consists of HTML (and related JavaScript or TypeScript) and C/C++ source. The C/C++ is compiled using `clang` into a `.wasm` binary module. The `.wasm` module is loaded as a WebAssembly module by your JavaScript using `twr-wasm`. ## JavaScript/TypeScript Part of Wasm Project -On the JavaScript side of your WebAssembly project you will use the twr-wasm JavaScript/TypeScript class `twrWasmModule` or `twrWasmModuleAsync` to load the `.wasm` module, and then call C functions in it using `callC` (more details are in the [TypeScript/Javascript API section](../api/api-typescript.md)). +On the JavaScript side of your WebAssembly project you will use the twr-wasm JavaScript/TypeScript class `twrWasmModule` or `twrWasmModuleAsync` to load the `.wasm` module, and then call C functions in it using `callC` (more details are in the [TypeScript/Javascript API section](../api/api-ts-modules.md)). ## C/C++ Part of Wasm Project You will call C functions (or C++ with ' extern "C" ' linkage) in the `.wasm` module from your JavaScript. You can also call JavaScript functions from your C/C++ code, but this is less common. There is no direct equivalent to a C "main". Instead, a Wasm module provides exported C functions that you can call from JavaScript/TypeScript. A Wasm module is more like a runtime loaded dynamic library. -You're C/C++ code can be non-blocking or blocking. Blocking means that it "takes a long time" to return. For example, if you want to send mouse events to C code, have the code process them then return, this would be non-blocking. Alternately, if your C code is a big loop that never returns, that would be very blocking. You can use the twr-wasm class `twrWasmModuleAsync` to execute blocking code from JavaScript. The example [maze](../examples/examples-maze.md) demonstrates both non-blocking and blocking C calls. +`twr-wasm` supports C/C++ code that is either asynchronous (non-blocking) or syncronous (blocking). A CLI app is an example of typical blocking code. A CLI app typically blocks waiting for keyboard input (blocking means that it "takes a long time" to return). If your C code is a big loop that never returns, that would be very blocking. Alternately,if you send mouse events to C code, have the code process them then return, this would be non-blocking. You can use the twr-wasm class `twrWasmModuleAsync` to execute blocking code from JavaScript or `twrWasmModule` to integrate asynchronous C/C++ code. The example [maze](../examples/examples-maze.md) demonstrates both non-blocking and blocking C calls. -Here are some examples of different types of C/C++ code: - -- If you're C/C++ code does not have any direct user interface built in, it can do its calculations and return. The [FFT](../examples/examples-fft.md) is an example of this. -- If your C/C++ code uses a classic C "UI", where it gets keys from stdin and sends the results to stdout, you can direct stdin and stdout to a `
` or `` tag. This is explained in the [stdio](../gettingstarted/stdio.md) section. -- Your C/C++ code could be sent events from JavaScript (such mouse, key, timer, or other). This is done by simply calling a C function with the events as arguments. The C/C++ code could then generate no output, could render to a `
` or `` using stdio type C/C++ functions, or it could render to a `` using 2D drawing APIs that correspond to JavaScript canvas 2D draw operations. ([Balls](../examples/examples-balls.md)) is an example. +See the [examples](../examples/examples-overview.md) of different types of C/C++ apps. ## Steps to integrate C code with JavaScript code Here are the general steps to integrate your C with your JavaScript: -1. [Compile your C code](compiler-opts.md) with clang and link with wasm-ld to create the ``.wasm`` file. +1. [Compile your C code](compiler-opts.md) with `clang` and link with `wasm-ld` to create the `.wasm` file. 2. On the JavaScript side you: - 1. Access twr-wasm "ES" modules in the normal way with `import`. + 1. Access `twr-wasm` "ES" modules in the normal way with `import`. 2. Add a `
` or `` to your HTML ([see stdio](stdio.md)) - 3. Use `new twrWasmModule()`, followed by a call to `loadWasm()`, then one or more `callC()`. - 4. Alternately, use `twrWasmModuleAsync()` -- which is interchangeable with twrWasmModule, but proxies through a worker thread, and adds blocking support, including blocking char input. + 3. Use `new twrWasmModule`, followed by a call to `loadWasm`, then one or more `callC`. + 4. Alternately, use `twrWasmModuleAsync` -- which is interchangeable with `twrWasmModule`, but proxies through a worker thread, which allows you to call blocking functions from the asynchronous JavaScript main thread. 5. For more details, see the remainder of this documentation, or see the [hello world](../examples/examples-helloworld.md) or [other exampes](../examples/examples-overview.md). diff --git a/docs/gettingstarted/charencoding.md b/docs/gettingstarted/charencoding.md index 6a52221d..ab100f95 100644 --- a/docs/gettingstarted/charencoding.md +++ b/docs/gettingstarted/charencoding.md @@ -14,22 +14,27 @@ setlocale(LC_ALL, "") This will change the C locale language to the one selected in the browser, and will enable consistent UTF-8 character encoding support. -Without this line, the standard C runtime will (mostly) default character encoding to ASCII, as per the standard. The exception is that just as with gcc, twr-wasm consoles support **outputting** UTF-8. +Without this line, the standard C runtime will default character encoding to ASCII, as per the std c lib standard. However, just as with gcc, twr-wasm consoles support outputting UTF-8, even in the default setting. ## Character Encodings twr-wasm supports ASCII, UNICODE, and extended-ASCII (in the form of Windows-1252). +### UTF-8 These days UNICODE with UTF-8 encoding is the most popular method of displaying and encoding text. UTF-8 is popular because it has the deep character glyph definitions of UNICODE with an encoding that provides (a) the best backwards compatibility with ASCII, and (b) a compact memory footprint. It does this at the expense of some multibyte complexities. UTF-8 is variable length, and uses between one to four bytes to represent any unicode code point, with ASCII compatibility in the first 128 characters. It is also the standard for the web, and the default for clang. But because UTF-8 uses a variable number of bytes per character it can make string manipulation in C a bit harder than ASCII, Windows-1252 or UTF-32. +### Locale In this document you will see the term "locale". This term originated (at least as its commonly used in programming) in the standard C library, and is also used in the standard C++ library (libc++ in twr-wasm). A locale refers to a region of the world, along with a specific character encoding. The twr-wasm standard c runtime uses a label akin to this to define a locale: `en-US.UTF-8`. Of note is that libc++ and the standard C runtime have different domains for their locales (ie, they don't directly impact each other). You can learn more about locales by searching the internet. -twr-wasm C locales support ASCII, UTF-8 or windows-1252 character encoding. UTF-16/32 are not supported as a std c lib locale setting, but functions are provided to convert utf-32 (unicode code points) to and from ASCII, UTF-8, and windows-1252 "code pages" (there are other miscellaneous utf-32 based functions as well.) +twr-wasm C locales support ASCII, UTF-8 or windows-1252 character encoding. -Although twr-wasm's standard c library locale doesn't support utf-32 directly, you can use int arrays (instead of byte arrays) to hold utf-32 strings, and then convert them to/from utf-8 with the help of the provided functions for this purpose. Alternately, you can use libc++, which has classes that directly support utf-16 and utf-32. +### UTF-32 +UTF-16/32 are not supported as a std c lib locale setting, but functions are provided to convert utf-32 (unicode code points) to and from ASCII, UTF-8, and windows-1252 "code pages" (there are other miscellaneous utf-32 based functions as well.) -## Windows Compatibility with Windows-1252 +You can also use libc++, which has classes that directly support utf-16 and utf-32. + +### Windows Compatibility with Windows-1252 Windows-1252 is the default character encoding on Windows computers in many countries - particularly the Americas and western Europe -- and particularly when using MSVC. Linux, clang, gcc, and the web commonly default in some way to UTF-8 character encoding. Windows-1252 is an extension of ASCII that uses a single byte per character. This makes it easier than UTF-8 from a programmers perspective, but it doesn't represent as many characters. It is supported by twr-wasm to make it easier to port legacy C code, windows code, as well as a simpler alternative to UTF-8. twr-wasm supports Windows-1252, and you can enable it like this: @@ -49,6 +54,6 @@ These days text editors generally default to UTF-8. In order to use windows-125 By default, the Microsoft Visual Studio C compiler (MSVC) does not treat string literals as UTF-8. Instead, it treats them as being encoded in the current code page of the system, which is typically Windows-1252 on western european language Windows systems. twr-wasm is designed to work with clang, which does default to utf-8, so if you are compiling code written for MSVC, and you use extend character sets (non ASCII), you may need to adjust your compiler settings with the flags mentioned above. ## More -For more details see [Localization Reference for twr-wasm](../api/api-localization.md) +For more details see [Localization Reference for twr-wasm](../api/api-c-localization.md) diff --git a/docs/gettingstarted/compiler-opts.md b/docs/gettingstarted/compiler-opts.md index f6ab0b99..b752a19b 100644 --- a/docs/gettingstarted/compiler-opts.md +++ b/docs/gettingstarted/compiler-opts.md @@ -89,7 +89,7 @@ If you are using `twrWasmModuleAsync`, shared memory must also be enabled. Like --shared-memory --no-check-features --initial-memory=1048576 --max-memory=1048576 ~~~ -See this [production note on using shared memory](../more/production.md). +See this [note on CORS headers with shared memory](../more/production.md). ### Stack Size You can change your C/C++ stack size from the default 64K with the following `wasm-ld` option. This example sets the stack at 128K diff --git a/docs/gettingstarted/debugging.md b/docs/gettingstarted/debugging.md index e18bff38..9aa667c7 100644 --- a/docs/gettingstarted/debugging.md +++ b/docs/gettingstarted/debugging.md @@ -31,7 +31,7 @@ If you are having issues with import resolution, [see this section.](../more/imp ## Useful twr-wasm Debug Functions Use `twr_conlog` to print to the JavaScript console from C (see API ref section). ~~~c -#include "twr-wasm.h" +#include "twr-crt.h" twr_conlog("hello 99 in hex: %x",99); ~~~ diff --git a/docs/gettingstarted/events.md b/docs/gettingstarted/events.md new file mode 100644 index 00000000..151d6785 --- /dev/null +++ b/docs/gettingstarted/events.md @@ -0,0 +1,65 @@ +--- +title: Using events in C/C++ and WebAssembly with twr-wasm +description: Certain twr-wasm APIs support events. This section describes how events function. +--- + +# Overview of Events +This section describes how to use twr-wasm to: + +- register event callbacks in C/C++ +- use events in C/C++ + +## Quick Example +~~~c title="timer events" +#include +#include "twr-crt.h" + +int t2_count=0; +int t2_id; + +// timer2 event callback (called multiple times) +__attribute__((export_name("on_timer2"))) +void on_timer2(int event_id) { + t2_count++; + printf("timer callback 2 entered (event id=%d, count=%d)\n", event_id, t2_count); + + if (t2_count==5) { + twr_timer_cancel(t2_id); + printf("timer example complete\n") + } +} + +// C entry point to call from JavaScript +int timer_main() { + printf("the timer will trigger 5 times...\n"); + + int t2_eventid=twr_register_callback("on_timer2"); + t2_id=twr_timer_repeat(500, t2_eventid); +} +~~~ + +## Examples +| Name | View Live Link | Source Link | +| --------- | ------------ | ----------- | +| timer example | [View timer test](/examples/dist/tests-timer/index.html) | [Source](https://github.com/twiddlingbits/twr-wasm/tree/main/examples/test-timer) | +| library example | [View library example](/examples/dist/lib/index.html) | [Source](https://github.com/twiddlingbits/twr-wasm/tree/main/examples/lib) | + +# Events +In twr-wasm, certain APIs can trigger events. For example a timer can trigger a "timer complete" event, or an audio api my trigger a "file has finished playing" event. An event had an `id` and an associated callback. The `id` is an integer that identifies the event (`int event_id`). In order to receive an event call back: + +1. Write your callback in C/C++. It must be C linkage, and you should be exported from your C code to JavaScript/TypeScript using the `export_name` clang `__attribute__` like this: `__attribute__((export_name("on_timer2")))`. Replace `on_timer2` with your callback function name. +2. Register your callback. This will also allocate the event ID paired to this callback. For example: `int t2_event_id=twr_register_callback("on_timer2");` +3. Call an API that takes an event `id`. For example: `twr_timer_repeat(500, t2_eventid);`. This will call the `t2_event` callback every 500ms. + +You can use the same event/callback with multiple APIs if you wish. When the event callback is called, the first argument will be the event `id` triggering the callback. There may then be optional parameters. These are event specific. + +As in JavaScript, twr-wasm event callbacks only occur when your C/C++ code is not running. + +# When using twrWasmModuleAsync + +With a `twrWasmModuleAsync` module, various blocking APIs are available. For example: `twr_sleep`. When these functions are blocking (waiting), event callbacks are queued and not processed until your C functions return back to JavaScript. + +With a `twrWasmModuleAsync` module, events are sent from the JavaScript main thread to the worker thread that is running the C/C++ code. + +# twr_register_callback +See [twr_register_callback](../api/api-c-general.md#twr_register_callback) \ No newline at end of file diff --git a/docs/gettingstarted/parameters.md b/docs/gettingstarted/parameters.md index 1de1f553..6e9d01e9 100644 --- a/docs/gettingstarted/parameters.md +++ b/docs/gettingstarted/parameters.md @@ -79,23 +79,23 @@ const structSize=12; const structIndexA=0; const structIndexB=4; const structIndexC=8; // compiler allocates pointer on 4 byte boundaries -let structMem=await mod.malloc(structSize); -let intMem=await mod.malloc(4); -mod.setLong(structMem+structIndexA, 1); -mod.mem8[structMem+structIndexB]=2; // you can access the memory directly with the mem8, mem32, and memD (float64 aka double) byte arrays. -mod.setLong(structMem+structIndexC, intMem); -mod.setLong(intMem, 200000); +let structMem=mod.wasmMem.malloc(structSize); +let intMem=mod.wasmMem.malloc(4); +mod.wasmMem.setLong(structMem+structIndexA, 1); +mod.wasmMem.mem8[structMem+structIndexB]=2; // you can access the memory directly with the mem8, mem32, and memD (float64 aka double) byte arrays. +mod.wasmMem.setLong(structMem+structIndexC, intMem); +mod.wasmMem.setLong(intMem, 200000); ~~~ note that: -- `await mod.malloc(structSize)` is a shortcut for: `await mod.callC(["malloc", structSize])` -- `mod.malloc` returns a C pointer as a `number`. This pointer is also an index into `WebAssembly.Memory` -- which is exposed as the byte array (`Uint8Array`) via `mod.mem8` by twr-wasm. +- `mod.wasmMem.malloc(structSize)` is a shortcut for: `mod.callC(["malloc", structSize])` +- `mod.wasmMem.malloc` returns a C pointer as a `number`. This pointer is also an index into `WebAssembly.Memory` -- which is exposed as the byte array (`Uint8Array`) via `mod.wasmMem.mem8` by twr-wasm. - When accessing a C `struct` in JavaScript/TypeScript, you have to do a bit of arithmetic to find the correct structure entry. - The entry `int *c` is a pointer to an `int`. So a separate `malloc` to hold the `int` is needed. -- In twr-wasm there is no function like `setLong` to set a byte. Instead you access the byte array view of the WebAssembly memory with `mod.mem8`. Functions like `mod.setLong` manipulate this byte array for you. -- As well as `mod.mem8` (Uint8Array), you can also access WebAssembly.Memory directly via `mod.mem32` (Uint32Array), and `mod.memD` (Float64Array). -- The list of functions available to access WebAssembly.Memory can be [found at the end of this page.](../api/api-typescript.md) +- In twr-wasm there is no function like `setLong` to set a byte. Instead you access the byte array view of the WebAssembly memory with `mod.wasmMem.mem8`. Functions like `mod.wasmMem.setLong` manipulate this byte array for you. +- As well as `mod.wasmMem.mem8` (Uint8Array), you can also access WebAssembly.Memory directly via `mod.wasmMem.mem32` (Uint32Array), and `mod.wasmMem.memD` (Float64Array). +- The list of functions available to access WebAssembly.Memory can be [found at the end of this page.](../api/api-ts-memory.md) ### Passing struct to C from JavaScript @@ -117,16 +117,16 @@ Once the `struct` has been created in JavaScript, you can call the C function `d await mod.callC(["do_struct", structMem]); // will add two to each value ~~~ -### Accessing returned C struct in JavaScript +### Reading C struct in JavaScript -You access the returned elements like this using JavaScript: +You read the modified elements like this using JavaScript: ~~~js -success=mod.getLong(structMem+structIndexA)==3; -success=success && mod.mem8[structMem+structIndexB]==4; -const intValPtr=mod.getLong(structMem+structIndexC); +success=mod.wasmMem.getLong(structMem+structIndexA)==3; +success=success && mod.wasmMem.mem8[structMem+structIndexB]==4; +const intValPtr=mod.wasmMem.getLong(structMem+structIndexC); success=success && intValPtr==intMem; -success=success && mod.getLong(intValPtr)==200002; +success=success && mod.wasmMem.getLong(intValPtr)==200002; ~~~ You can see the additional complexity of de-referencing the `int *`. @@ -135,8 +135,8 @@ You can see the additional complexity of de-referencing the `int *`. You can free the malloced memory like this: ~~~js -await mod.callC(["free", intMem]); // unlike malloc, there is no short cut for free, yet -await mod.callC(["free", structMem]); +mod.wasmMem.free(intMem); +mod.wasmMem.free(structMem); ~~~ The complete code for this [example is here](../examples/examples-callc.md/). @@ -161,10 +161,10 @@ mod.callC(["my_function", "this is my string"]); // mod is instance of twrWasmM Under the covers, to pass "this is my string" from JavaScript to the C Web Assembly function, `callC` will execute code like this: ~~~js -// twrWasmModule member function -async putString(sin:string, codePage = codePageUTF8) { +// twrWasmMemory member function +putString(sin:string, codePage = codePageUTF8) { const ru8 = this.stringToU8(sin, codePage); // convert a string to UTF8 encoded characters stored in a Uint8Array - const strIndex = await this.malloc(ru8.length + 1); // shortcut for: await this.callC(["malloc", ru8.length + 1]); + const strIndex = this.malloc(ru8.length + 1); // shortcut for: await this.callC(["malloc", ru8.length + 1]); this.mem8.set(ru8, strIndex); // mem8 is of type Uint8Array and is the Wasm Module’s Memory this.mem8[strIndex + ru8.length] = 0; return strIndex; @@ -186,7 +186,7 @@ twr-wasm provides a function to pull the string out of WebAssembly Memory and co ~~~js const retStringPtr = await mod.callC(["ret_string_function"]); -console.log(mod.getString(retStringPtr)); +console.log(mod.wasmMem.getString(retStringPtr)); ~~~ The `retStringPtr` is an integer 32 (but converted to a JavaScript `number`, which is Float 64). This integer is an index into the WebAssembly Memory. @@ -194,7 +194,7 @@ The `retStringPtr` is an integer 32 (but converted to a JavaScript `number`, whi ## Passing ArrayBuffers from JavaScript to C/C++ WebAssembly When `callC` in twr-wasm is used to pass an ArrayBuffer to and from C/C++, some details are handled for you. The technique is similar to that used for a `string` or as performed manually for a `struct` above, with the following differences: - - `ArrayBuffers` have entries of all the same length, so the index math is straight forward and now `struct` padding is needed. + - `ArrayBuffers` have entries of all the same length, so the index math is straight forward and no `struct` padding is needed. - When an `ArrayBuffer` is passed to a function, the function receives a pointer to the `malloc` memory. If the length is not known by the function, the length needs to be passed as a separate argument. - Before `callC` returns, any modifications made to the memory by the C code are reflected back into the `ArrayBuffer`. - the malloced copy of the ArrayBuffer is freed. diff --git a/docs/gettingstarted/stdio.md b/docs/gettingstarted/stdio.md index 0f4f8370..3074b77d 100644 --- a/docs/gettingstarted/stdio.md +++ b/docs/gettingstarted/stdio.md @@ -3,11 +3,11 @@ title: Consoles with C/C++ WebAssembly (stdio, stderr, more) description: Stream characters to a div or canvas tag. Input from stdin. Configure a canvas tag as a terminal-console. With twr-wasm lib. --- -# Consoles with C/C++ WebAssembly
stdio, stderr, and more -This section describes how to use twr-wasm in order to: +# Overview of Consoles +This section describes how to use twr-wasm to: -- create input/output consoles for use by C/C++ -- direct stdin/stdout and stderr to a console +- create input/output consoles for use by C/C++ with WebAssembly +- direct stdin, stdout and stderr to a console - use addressable display and canvas 2D consoles - use multiple consoles at once @@ -86,30 +86,36 @@ If neither of the above `
` or `` is defined in your HTML, and if yo ## Console Classes Consoles are implemented in TypeScript and run in the JavaScript main thread. This allows consoles to be shared by multiple wasm modules. -For simple cases, when you use the tag shortcuts, you won't need to use these console classes directly. For more bespoke cases, they will come in handy. For details on console classes, see the [TypeScript/JavaScript API reference](../api/api-typescript.md#console-classes) +For simple cases, when you use the tag shortcuts, you won't need to use these console classes directly. For more bespoke cases, they will come in handy. For details on console classes, see the [TypeScript/JavaScript API reference](../api/api-ts-consoles.md) These conosle classes are available in twr-wasm: -- [`twrConsoleDiv`](../api/api-typescript.md#class-twrconsolediv) streams character input and output to a div tag -- [`twrConsoleTerminal`](../api/api-typescript.md#class-twrconsoleterminal) provides streaming or addressable character input and output using a canvas tag. -- [`twrConsoleDebug`](../api/api-typescript.md#class-twrconsoledebug) streamings characters to the browser debug console. -- [`twrConsoleCanvas`](../api/api-typescript.md#class-twrconsolecanvas) creates a 2D drawing surface that the Canvas compatible [2d drawing APIs](../api/api-c-d2d.md) can be used with. +- [`twrConsoleDiv`](../api/api-ts-consoles.md#class-twrconsolediv) streams character input and output to a div tag +- [`twrConsoleTerminal`](../api/api-ts-consoles.md#class-twrconsoleterminal) provides streaming or addressable character input and output using a canvas tag. +- [`twrConsoleDebug`](../api/api-ts-consoles.md#class-twrconsoledebug) streamings characters to the browser debug console. +- [`twrConsoleCanvas`](../api/api-ts-consoles.md#class-twrconsolecanvas) creates a 2D drawing surface that the Canvas compatible [2d drawing APIs](../api/api-c-d2d.md) can be used with. ## Multiple Consoles with Names -When you instantiate a class `twrWasmModule` or `twrWasmModuleAsync`, you can pass it the module option `io` -- a javascript object containing name-console attributes. Your C/C++ code can then retrieve a console by name. This is described in more detail the [TypeScript/JavaScript API Reference.](../api/api-typescript.md#io-option-multiple-consoles-with-names) +When you instantiate a class `twrWasmModule` or `twrWasmModuleAsync`, you can pass it the module option `io` -- a javascript object containing name-console attributes. Your C/C++ code can then retrieve a console by name. This is described in more detail the [TypeScript/JavaScript API Reference.](../api/api-ts-modules.md#io-option-multiple-consoles-with-names) Also see the [multi-io example](../examples/examples-multi-io.md). ## Setting stdio and stderr -`stdio` can be defined automatically if you use a Tag Shortcut. `stderr` streams to the browser's debug console by default. Both can be set to a specific console [with the module `io` option.](../api/api-typescript.md#io-option-multiple-consoles-with-names) +`stdio` can be defined automatically if you use a Tag Shortcut. `stderr` streams to the browser's debug console by default. Both can be set to a specific console [with the module `io` option.](../api/api-ts-modules.md#io-option-multiple-consoles-with-names) -For example, either of these will set `stdio` to a streaming `div` console: +For example, given: ~~~c const tag=document.getElementById("console-tag"); const streamConsole=new twrConsoleDiv(tag); +~~~ + +Either of these will set `stdio` to a streaming `div` console: +~~~c const mod = new twrWasmModule({stdio: streamConsole}); +~~~ +~~~c const mod = new twrWasmModule({ io: {stdio: streamConsole} }); ~~~ @@ -132,7 +138,7 @@ Consoles can support UTF-8 or Windows-1252 character encodings (see [Character E `d2d_functions` [are available to operate on](../api/api-c-d2d.md) Canvas 2D Consoles. ### Reading from a Console -Reading from a console is blocking, and so [`twrWasmModuleAsync` must be used to receive keys.](../api/api-typescript.md#class-twrwasmmoduleasync) There are some specific requirements to note in the `twrWasmModuleAsync` API docs. +Reading from a console is blocking, and so [`twrWasmModuleAsync` must be used to receive keys.](../api/api-ts-modules.md#class-twrwasmmoduleasync) There are some specific requirements to note in the `twrWasmModuleAsync` API docs. You can get characters with any of these functions: @@ -171,7 +177,7 @@ For example: fprintf(stderr, "hello over there in browser debug console land\n"); ~~~ -A more common method to send output to the debug console is to use `twr_conlog`. See [General C API Section](../api/api-c-general.md#twr_conlog). +A more common method to send output to the debug console [is to use `twr_conlog`.](../api/api-c-general.md#twr_conlog) diff --git a/docs/img/readme-img-2-player-pong.png b/docs/img/readme-img-2-player-pong.png new file mode 100644 index 00000000..fdb2cbe5 Binary files /dev/null and b/docs/img/readme-img-2-player-pong.png differ diff --git a/docs/img/readme-img-menu-pong.png b/docs/img/readme-img-menu-pong.png new file mode 100644 index 00000000..1e7ce594 Binary files /dev/null and b/docs/img/readme-img-menu-pong.png differ diff --git a/docs/img/readme-img-pong.png b/docs/img/readme-img-single-player-pong.png similarity index 100% rename from docs/img/readme-img-pong.png rename to docs/img/readme-img-single-player-pong.png diff --git a/docs/index.md b/docs/index.md index 42896325..1c3e1cc3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,45 +1,45 @@ --- title: Easier WebAssembly with twr-wasm - Documentation and Examples -description: An easier way to create C/C++ WebAssembly. Unlike emscripten, use clang directly. Examples of blocking functions, 2D drawing, char I/O with
tag, etc. +description: An easier way to create C/C++ WebAssembly. await on blocking C/C++ code, 2D drawing and audio APIs, char I/O with
tag, more. --- # Easier WebAssembly with twr-wasm
Documentation and Examples - -## Easier C/C++ WebAssembly -Version 2.4.2 +Version 2.5.0 -twr-wasm is a simple, lightweight and easy to use library for building C/C++ WebAssembly code directly with clang. It solves some common use cases with less work than the more feature rich emscripten. +twr-wasm is a simple, lightweight and easy to use library for building C/C++ WebAssembly code directly with clang. Run C/C++ code in a web browser. Legacy code, libraries, full applications, or single functions can be integrated with JavaScript and TypeScript. twr-wam solves some common use cases with less work than the more feature rich emscripten. -twr-wasm includes comprehensive console support for `stdio`. You can input and print to a `
` tag, or use a `` element as an terminal. +**Key Features:** -twr-wasm makes it easy to `await` on blocking C/C++ functions. +- build `.wasm` modules using C/C++ using clang directly (no wrapper) +- from JavaScript load `.wasm` modules, call C/C++ functions, and access wasm memory +- comprehensive console support for `stdin`, `stdio`, and `stderr`. -twr-wasm makes it easy to use C/C++ 2D drawing apis that are compatible with JavaScript Canvas APIs to draw to a `` element. + - in C/C++, print and get characters to/from `
` tags in your HTML page + - in C/C++, print and get characters to/from a `` based "terminal" + - localization support, UTF-8, and windows-1252 support -twr-wasm allows you to run C/C++ code in a web browser. Legacy code, libraries, full applications, or single functions can be integrated with JavaScript and TypeScript. +- the optional TypeScript `class twrWasmModuleAsync` can be used to: -twr-wasm is designed to be used with the standard llvm clang compiler and tools. + - integrate CLI C/C++ code with JavaScript + - In JavaScript `await` on blocking/synchronous C/C++ functions. + +- 2D drawing API for C/C++ compatible with JavaScript Canvas +- audio playback APIs for C/C++ +- create your own C/C++ APIs using TypeScript by extending `class twrLibrary` +- standard C library optimized for WebAssembly +- libc++ built for WebAssembly +- comprehensive examples and documentation ## Live WebAssembly Examples and Source | Name | View Live Link | Source Link | | --------- | ------------ | ----------- | | Bouncing Balls (C++) | [View bouncing balls](https://twiddlingbits.dev/examples/dist/balls/index.html) | [Source for balls](https://github.com/twiddlingbits/twr-wasm/tree/main/examples/balls) | -| Maze Gen/Solve (Win32 C Port) | [View live maze](https://twiddlingbits.dev/examples/dist/maze/index.html) | [Source for maze](https://github.com/twiddlingbits/twr-wasm/tree/main/examples/maze) | +| Pong (C++) | [Pong](https://twiddlingbits.dev/examples/dist/pong/index.html) | [Source](https://github.com/twiddlingbits/twr-wasm/tree/main/examples/pong) | | Input/Output with `
` | [View square demo](https://twiddlingbits.dev/examples/dist/divcon/index.html) | [Source](https://github.com/twiddlingbits/twr-wasm/tree/main/examples/divcon) | |I/O to terminal with ``|[View demo](https://twiddlingbits.dev/examples/dist/terminal/index.html) |[Source](https://github.com/twiddlingbits/twr-wasm/tree/main/examples/terminal) | |CLI using libc++ and ``)| [View console](https://twiddlingbits.dev/examples/dist/tests-user/index.html) | [Source](https://github.com/twiddlingbits/twr-wasm/tree/main/examples/tests-user) | -## Key Features -- compile and link C/C++ for use with WebAssembly using clang directly -- standard C library, libc++. and purpose built APIs available from C/C++ -- TypeScript/JavaScript classes to load Wasm modules and call C/C++ functions -- localization support, UTF-8, and windows-1252 support -- in C/C++, print and get characters to/from `
` tags in your HTML page -- in C/C++, print and get characters to/from a `` based "terminal" -- in C/C++ use 2D drawing API compatible with JavaScript Canvas -- in C/C++, use the "blocking loop" pattern and integrate with Javascript's asynchronous event loop - ## Hello World Here is the simplest `twr-wasm` example. @@ -75,14 +75,7 @@ void hello() { ## Why? The [Wasm Runtime Limitations](more/wasm-problem.md) section explains why a library like twr-wasm is needed to use WebAssembly. -## Version 2 vs. 1 - - libc++ built for WebAssembly is included - - most of the standard C library is now implemented - - instructions for WebAssembly C/C++ source level debugging - - version of library with debug symbols provided - - locale, UTF-8, and windows-1252 support - -## Version 2 Limitations +## Limitations - libc++ not built with exceptions enabled - some standard C library functions are not 100% implemented - Designed to work with a browser. Not tested with or designed to work with node.js diff --git a/docs/more/imports.md b/docs/more/imports.md index 9dc5e7da..1f8fa2c7 100644 --- a/docs/more/imports.md +++ b/docs/more/imports.md @@ -11,11 +11,14 @@ import {twrWasmModule} from "twr-wasm"; It can be confusing to determine how tools like VS Code, a bundler, or a Web Browser resolve imports like "twr-wasm". This section explains how it works, using the included examples as examples. -If you have installed `twr-wasm` using `npm`, most tools will automatically resolve imports by finding the `node_modules` folder. +Two situations are addressed in this document: (1) browser import resolution when not using a bundler, and (2) if you installed using `git clone`. -However, if you installed using `git clone`, then other steps will need to be taken. The examples are in the git repo tree, and as a result there may be no `node_modules` for the twr-wasm libraries. This will also be the case for your project, if you installed with `git clone`. +## If you installed using `git clone` +If you have installed `twr-wasm` using `npm`, most tools will automatically resolve imports by finding the `node_modules` folder. -## Import path resolution by the bundler +However, if you installed using `git clone`, then other steps will need to be taken to help a bundler, VS Code, and tsc resolve imports. This situation can apply to the examples, which are in the git repo tree, and as a result there may be no `node_modules` for the twr-wasm libraries. This will also be the case for your project, if you installed with `git clone`. + +### Import path resolution by the bundler A bundler will find the twr-wasm library using one of these methods: 1. If twr-wasm has been installed with npm install, the bundler will find the `node_modules` folder @@ -31,6 +34,14 @@ In the examples, the alias entry in the `package.json` exists so that the parcel If you are using a bundler, you don't need to add a ` ~~~ -## Import resolution by VS Code and tsc -VS Code Intellisense and the typescript compiler need to find modules. If twr-wasm is installed using `npm` into a `node_modules` folder, this is probably automatic. But if this is not the case, you can add a line to the `tsconfig.json` as follows (this example assumes the `tsconfig.json` is in a examples/example folder). See the maze example. -~~~ -"paths": { - "twr-wasm": ["./../../lib-js/index"] -} -~~~ - - diff --git a/examples/balls/Makefile b/examples/balls/Makefile index 3453ea0b..4baefd97 100644 --- a/examples/balls/Makefile +++ b/examples/balls/Makefile @@ -15,7 +15,7 @@ TWRCPPFLAGS := --target=wasm32 -fno-exceptions -fno-rtti -nostdlibinc -nostdinc # -g for debug symbols # -v verbose CPPLIB := ../twr-cpp -CFLAGS := -c -Wall -O3 -DNDEBUG $(TWRCPPFLAGS) -I $(CPPLIB) +CFLAGS := -c -Wall -O3 $(TWRCPPFLAGS) -I $(CPPLIB) CFLAGS_DEBUG := -c -Wall -g -O0 $(TWRCPPFLAGS) -I $(CPPLIB) OBJOUTDIR := out diff --git a/examples/balls/index.html b/examples/balls/index.html index a8590177..43a8e5df 100644 --- a/examples/balls/index.html +++ b/examples/balls/index.html @@ -30,6 +30,7 @@

WebAssembly with twr-wasm

+ + + +
+ + + + + \ No newline at end of file diff --git a/examples/lib/package.json b/examples/lib/package.json new file mode 100644 index 00000000..60b49279 --- /dev/null +++ b/examples/lib/package.json @@ -0,0 +1,11 @@ +{ + "@parcel/resolver-default": { + "packageExports": true + }, + "alias": { + "twr-wasm": "../../lib-js/index.js" + }, + "dependencies": { + "twr-wasm": "^2.0.0" + } +} diff --git a/examples/lib/testex.c b/examples/lib/testex.c new file mode 100644 index 00000000..36d4e3a2 --- /dev/null +++ b/examples/lib/testex.c @@ -0,0 +1,73 @@ + +#include +#include +#include "twr-crt.h" +#include "twr-ex.h" + +// This example shows how to create a twrWasmLibrary +// A twrLibrary enables you to expose JavaScript code as C APIs +// There are two required files for a new Library: +// A TypeScript file that derives from twrLibrary +// A C .h file that provide C API signatures for the functions defined in twr + +// This file is a test file that exercises the C APIs exposed by twrLibraryExample + + +// key event callback +__attribute__((export_name("on_key"))) +void on_key(int event_id, int key_code) { + printf("key code: %d\n", key_code); +} + +// timer event callback (called once) +__attribute__((export_name("on_timer1"))) +void on_timer1(int event_id) { + printf("timer callback 1 entered (event id=%d) !\n", event_id); + + printf("press keys now\n"); + int key=twr_register_callback("on_key"); + + ex_listen_key_events(key); +} + +// timer event callback (called multiple times by different timers) +__attribute__((export_name("on_timer2"))) +void on_timer2(int event_id) { + printf("timer callback 2 entered (event id=%d)\n", event_id); +} + +// entry point +__attribute__((export_name("twr_main"))) +void twr_main(int is_async) { + + printf("welcome to the example library using %s\n",is_async?"twrWasmModuleAsync":"twrWasmModule"); + + if (5.5!=return_a_float()) {printf("ERROR in return_a_float!\n"); return;} + if (5.55!=return_a_double()) {printf("ERROR in return_a_double!\n"); return;} + if (5!=return_a_int()) {printf("ERROR in return_a_int!\n"); return;} + + unsigned long ms=ex_get_epoch(); + printf ("ms since the epoch is %lu\n", ms); + + char* two_str=ex_append_two_strings("AAA-","BBB"); + printf ("two strings appended: %s\n", two_str);; + free(two_str); + + if (is_async) { + printf("going to sleep..."); + ex_sleep(1000); + printf("awake!\n"); + } + + int timer1=twr_register_callback("on_timer1"); + + // note these are all the same callback, but different id + int timer2=twr_register_callback("on_timer2"); + int timer3=twr_register_callback("on_timer2"); + int timer4=twr_register_callback("on_timer2"); + + ex_single_shot_timer(2000, timer1); + ex_single_shot_timer(500, timer2); + ex_single_shot_timer(1000, timer3); + ex_single_shot_timer(1500, timer4); +} diff --git a/examples/lib/tsconfig.json b/examples/lib/tsconfig.json new file mode 100644 index 00000000..13203847 --- /dev/null +++ b/examples/lib/tsconfig.json @@ -0,0 +1,36 @@ + { + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + "incremental": false, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + "composite": false, /* Enable constraints that allow a TypeScript project to be used with project references. */ + + /* Language and Environment */ + "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": ["ES2023","DOM"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + + /* Modules */ + "module": "ESNext", /* Specify what module code is generated. */ + "moduleResolution": "Bundler", + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "declarationMap": true, /* Create sourcemaps for d.ts files. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + "outDir": "./out", /* Specify an output folder for all emitted files. */ + + /* Interop Constraints */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "paths": { + "twr-wasm": ["./../../lib-js/index"] + } + }, + "files": ["./twrlibex.ts"] + } + + + \ No newline at end of file diff --git a/examples/lib/twr-ex.h b/examples/lib/twr-ex.h new file mode 100644 index 00000000..a6a8c340 --- /dev/null +++ b/examples/lib/twr-ex.h @@ -0,0 +1,13 @@ + + +// These functions are defined in twrlibex.ts +__attribute__((import_name("ex_listen_key_events"))) void ex_listen_key_events(int event_id); +__attribute__((import_name("ex_single_shot_timer"))) void ex_single_shot_timer(int ms, int event_id); +__attribute__((import_name("ex_get_epoch"))) unsigned long ex_get_epoch(void); +__attribute__((import_name("ex_append_two_strings"))) char* ex_append_two_strings(const char*s1, const char*s2); +__attribute__((import_name("ex_sleep"))) void ex_sleep(int ms); +__attribute__((import_name("return_a_float"))) float return_a_float(void); +__attribute__((import_name("return_a_double"))) double return_a_double(void); +__attribute__((import_name("return_a_int"))) int return_a_int(void); + + diff --git a/examples/lib/twrlibex.ts b/examples/lib/twrlibex.ts new file mode 100644 index 00000000..006777bb --- /dev/null +++ b/examples/lib/twrlibex.ts @@ -0,0 +1,118 @@ +import {IWasmModule, IWasmModuleAsync, twrLibrary, keyEventToCodePoint, TLibImports, twrLibraryInstanceRegistry} from "twr-wasm" + +// Libraries use default export +export default class twrLibExample extends twrLibrary { + id:number; + + imports:TLibImports = { + ex_listen_key_events:{}, + ex_single_shot_timer:{}, + ex_get_epoch:{}, + ex_append_two_strings:{isAsyncFunction: true}, + ex_sleep:{isAsyncFunction: true, isModuleAsyncOnly: true}, + return_a_float:{}, + return_a_double:{}, + return_a_int:{}, + }; + + // every library should have this line + libSourcePath = new URL(import.meta.url).pathname; + + constructor() { + // all library constructors should start with these two lines + super(); + this.id=twrLibraryInstanceRegistry.register(this); + } + + // Because this function is in the imports list above, it will be added to the imports list for + // both twrWasmModule and twrWasmModuleAsyncProxy. + // The callingMod argument is added by twrLibrary, it is not passed by the caller C function. + // + // imported functions always run in the JavaScript Main thread. So if your C code is executing in a twrWasmModuleAsyncProxy + // worker thread, a C call to an import twrLibrary function will result in a remote procedure call into twrWasmModuleAsync, + // which calls this function. + // + // An eventID is retrieved by the C function calling twr_register_callback, which is implemented by twrWasmLibrary + ex_listen_key_events(callingMod:IWasmModule|IWasmModuleAsync, eventID:number) { + + const keyEventListner = (event:KeyboardEvent) => { + const r=keyEventToCodePoint(event); // twr-wasm utility function + if (r) { + // postEvent can only post numbers -- no translation of arguments is performed prior to making the C event callback + // See ex_append_two_strings below for an example using strings. + callingMod.postEvent(eventID, r); + } + } + + document.addEventListener('keydown', keyEventListner); + } + + ex_single_shot_timer(callingMod:IWasmModule|IWasmModuleAsync, milliSeconds:number, eventID:number) { + setTimeout(()=>{ + callingMod.postEvent(eventID) + }, milliSeconds); + } + + ex_get_epoch(callingMod:IWasmModule|IWasmModuleAsync, eventID: number) { + const date = Date.now(); + + return date; + } + + // Two versions of ex_append_two_strings are implemented in this example -- an "extra" one that uses the "async" function keyword. + // Because this function is added to imports with the option isAsyncFunction, when called by twrWasmModuleAsyncProxy, + // ex_append_two_strings_async will be called with await (instead of ex_append_two_strings called with no await, the default). + // Note that when adding the function name to imports with the option isAsyncFunction, you use a single import with + // the base import name. The "_async" addition will be used as needed by the import logic. + // + // callingMod.wasmMem can be used to access module memory. WasmMemoryAsync will be passed if the calling module is twrWasmModuleAsync, + // while WasmMemory will be passed if the calling module is a twrWasmModule. + // "WasmMemoryAsync.putXX", are async functions, while "WasmMemory.putXX" are not. + // Thus a possible reason to use isAsyncFunction is to make a call to put memory functions like "await callingMod.wasmMem.putString". + // Functions like "callingMod.wasmMem.getString" are not async and thus do not need the option "isAsyncFunction". + ex_append_two_strings(callingMod:IWasmModule, str1Idx:number, str2Idx:number) { + const newStr=callingMod.wasmMem.getString(str1Idx)+callingMod.wasmMem.getString(str2Idx); + const rv=callingMod.wasmMem.putString(newStr); + return rv; + } + + async ex_append_two_strings_async(callingMod:IWasmModuleAsync, str1Idx:number, str2Idx:number) { + const newStr=callingMod.wasmMem.getString(str1Idx)+callingMod.wasmMem.getString(str2Idx); + const rv=await callingMod.wasmMem.putString(newStr); + return rv; + } + + // this function ex_sleep has two options set in its imports: isAsyncFunction, isModuleAsyncOnly. The result is that: + // (a) ex_sleep_async can/must use the async function keyword (similar to ex_append_two_strings), and + // (b) this function will not be available when using twrWasmModule (in other words, twrWasmModuleAsync must be used) + // These options are set because ex_sleep_async is a blocking function. That is, the C code will block when calling ex_sleep, until it returns. + // This is in contrast to ex_single_shot_timer, which returns immediately to the C code, but then sends an event when the timer if complete. + // Thus ex_single_shot_timer can be used in both twrWasmModule and twrWasmModuleAsync. + async ex_sleep_async(callingMod:IWasmModuleAsync, milliSeconds:number) { + const p = new Promise( (resolve)=>{ + setTimeout(()=>{ resolve() }, milliSeconds); + }); + + return p; + } + + // this is testing returning a float + return_a_float(mod: IWasmModule|IWasmModuleAsync) { + return 5.5; + } + + // this is testing returning a double + return_a_double(mod: IWasmModule|IWasmModuleAsync) { + return 5.55; + } + + // this is testing returning a int32 + return_a_int(mod: IWasmModule|IWasmModuleAsync) { + return 5.55; + } + + //TODO!! add twr_register_event + //TODO!! I tired adding isAsyncProxyOverride, but it presents the difficulty function to call (which is derived from twrLibrary), is not accessible to twrLibraryProxy. Consider dynamic import? +} + + diff --git a/examples/maze/index.html b/examples/maze/index.html index 1006b2e0..47123fd5 100644 --- a/examples/maze/index.html +++ b/examples/maze/index.html @@ -2,7 +2,7 @@ Maze Generator/Solver - C using WebAssembly - + - +
+ diff --git a/examples/pong/jsEventsLib.ts b/examples/pong/jsEventsLib.ts new file mode 100644 index 00000000..6ff08d74 --- /dev/null +++ b/examples/pong/jsEventsLib.ts @@ -0,0 +1,106 @@ +import {IWasmModule, IWasmModuleAsync, twrLibrary, keyEventToCodePoint, TLibImports, twrLibraryInstanceRegistry} from "twr-wasm" + +// Libraries use default export +export default class jsEventsLib extends twrLibrary { + id: number; + + imports:TLibImports = { + registerKeyUpEvent: {}, + registerKeyDownEvent: {}, + registerAnimationLoop: {}, + registerMousePressEvent: {}, + registerMouseMoveEvent: {}, + + setElementText: {}, + }; + + // every library should have this line + libSourcePath = new URL(import.meta.url).pathname; + + constructor() { + super(); + this.id=twrLibraryInstanceRegistry.register(this); + } + + registerKeyUpEvent(callingMod:IWasmModule|IWasmModuleAsync, eventID: number) { + const keyEventListner = (event:KeyboardEvent) => { + const r=keyEventToCodePoint(event); // twr-wasm utility function + if (r) { + console.log(event, r); + callingMod.postEvent(eventID, r); + } + } + + document.addEventListener('keyup', keyEventListner); + } + + registerKeyDownEvent(callingMod:IWasmModule|IWasmModuleAsync, eventID: number) { + const keyEventListner = (event:KeyboardEvent) => { + const r=keyEventToCodePoint(event); // twr-wasm utility function + if (r) { + callingMod.postEvent(eventID, r); + } + } + + document.addEventListener('keydown', keyEventListner); + } + + registerAnimationLoop(callingMod:IWasmModule|IWasmModuleAsync, eventID: number) { + const loop: FrameRequestCallback = (time) => { + callingMod.postEvent(eventID, time); + requestAnimationFrame(loop); + } + requestAnimationFrame(loop); + } + + registerMouseMoveEvent(callingMod:IWasmModule|IWasmModuleAsync, eventID: number, elementIDPtr: number, relative: boolean) { + const elementID = callingMod.wasmMem.getString(elementIDPtr); + const element = document.getElementById(elementID)!; + + if (element == null) throw new Error("registerMouseEvent was given a non-existant element ID!"); + + if (relative) { + const x_off = element.offsetLeft; + const y_off = element.offsetTop; + element.addEventListener('mousemove', (e) => { + callingMod.postEvent(eventID, e.clientX - x_off, e.clientY - y_off); + }); + } else { + element.addEventListener('mousemove', (e) => { + callingMod.postEvent(eventID, e.clientX, e.clientY); + }); + } + } + + registerMousePressEvent(callingMod:IWasmModule|IWasmModuleAsync, eventID: number, elementIDPtr: number, relative: boolean) { + const elementID = callingMod.wasmMem.getString(elementIDPtr); + const element = document.getElementById(elementID)!; + + if (element == null) throw new Error("registerMouseEvent was given a non-existent element ID!"); + + if (relative) { + const x_off = element.offsetLeft; + const y_off = element.offsetTop; + element.addEventListener('mousedown', (e) => { + callingMod.postEvent(eventID, e.clientX - x_off, e.clientY - y_off, e.button); + }); + } else { + element.addEventListener('mousedown', (e) => { + callingMod.postEvent(eventID, e.clientX, e.clientY, e.button); + }); + } + } + + setElementText(mod:IWasmModule|IWasmModuleAsync, elementIDPtr: number, textPtr: number) { + const elementID = mod.wasmMem.getString(elementIDPtr); + const text = mod.wasmMem.getString(textPtr); + + const element = document.getElementById(elementID)!; + if (!element) throw new Error(`setElementText was given an invalid ID (${elementID})`); + + element.innerText = text; + } + +} + + diff --git a/examples/pong/pong-menu.cpp b/examples/pong/pong-menu.cpp new file mode 100644 index 00000000..f47ebb79 --- /dev/null +++ b/examples/pong/pong-menu.cpp @@ -0,0 +1,238 @@ +#include "pong-menu.h" +#include +#include /* malloc, free, rand */ + +template +LinkedListRoot::LinkedListRoot() { + this->root = NULL; + this->tail = NULL; +} + +// template +// LinkedListRoot::~LinkedListRoot() { +// while (this->root) { +// LinkedList* tmp = this->root->next; +// free(this->root); +// this->root = tmp; +// } +// this->root = NULL; +// this->tail = NULL; +// } + +template +void LinkedListRoot::addNode(T val) { + LinkedList* node = (LinkedList*)malloc(sizeof(LinkedList)); + node->next = NULL; + node->val = val; + if (!this->root) { + this->root = node; + this->tail = node; + } else { + this->tail->next = node; + this->tail = node; + } +} + +Menu::Menu() {} + +void Menu::setBounds(long width, long height) { + this->width = width; + this->height = height; + + #define NUM_BUTTONS 3 + const char* BUTTON_NAMES[NUM_BUTTONS] = { + "Single Player Pong", "2 Player Pong (AI)", "2 Player Pong" + }; + const int BUTTON_WIDTH = 400; + const int BUTTON_HEIGHT = 60; + const int BUTTON_SPACING = 40; + int button_offset = (width - BUTTON_WIDTH)/2; + int y_offset = (height - (BUTTON_HEIGHT)*NUM_BUTTONS - (BUTTON_SPACING)*(NUM_BUTTONS - 1))/2; + + for (int i = 0; i < NUM_BUTTONS; i++) { + int y = y_offset + (BUTTON_SPACING+BUTTON_HEIGHT)*i; + this->addButton(button_offset, y, BUTTON_WIDTH, BUTTON_HEIGHT, BUTTON_NAMES[i], i); + } +} + +void Menu::mouseMoveEvent(long x, long y) { + // printf("move: %ld, %ld\n", x, y); + switch (state) { + case MenuState::Menu: + this->updateButtonSelections(x, y); + break; + + default: + break; + } + +} + +void Menu::mousePressEvent(long x, long y) { + // printf("press: %ld, %ld\n", x, y); + switch (state) { + case MenuState::Menu: + this->tryButtonPress(x, y); + break; + + default: + break; + } +} + +void Menu::keyDownEvent(long keycode) { + switch (state) { + case MenuState::Menu: + break; + + case MenuState::SinglePlayerPong: + this->s_pong.keyDownEvent(keycode); + break; + + case MenuState::TwoPlayerPong: + this->t_pong.keyDownEvent(keycode); + break; + + default: + break; + } +} + +void Menu::keyUpEvent(long keycode) { + switch (state) { + case MenuState::Menu: + break; + + case MenuState::SinglePlayerPong: + this->s_pong.keyUpEvent(keycode); + break; + + case MenuState::TwoPlayerPong: + this->t_pong.keyUpEvent(keycode); + break; + + default: + break; + } +} + +void Menu::render(long delta) { + switch (state) { + case MenuState::Menu: + { + this->canvas.startDrawSequence(); + + this->canvas.reset(); + this->canvas.setFillStyleRGB(0xA0A0A0); + this->canvas.fillRect(0.0, 0.0, this->width, this->height); + + this->renderButtons(); + + this->canvas.endDrawSequence(); + } + break; + + case MenuState::SinglePlayerPong: + this->s_pong.tick(delta); + this->s_pong.render(); + break; + + case MenuState::TwoPlayerPong: + this->t_pong.tick(delta); + this->t_pong.render(); + break; + + default: + break; + } +} + +void Menu::addButton(long x, long y, long w, long h, const char* name, int id) { + MenuButton button = { + .x = x, + .y = y, + .w = w, + .h = h, + .name = name, + .id = id, + .selected = false, + .initialized = false, + }; + + this->buttons.addNode(button); +} + +void Menu::renderButtons() { + for (LinkedList* node = this->buttons.root; node; node = node->next) { + MenuButton* button = &(node->val); + this->canvas.setFillStyleRGB(button->selected ? 0x007007 : 0x0000FF); + this->canvas.fillRect(button->x, button->y, button->w, button->h); + + this->canvas.setFont("24px Serif"); + + if (!button->initialized) { + button->initialized = true; + d2d_text_metrics text_metrics; + this->canvas.measureText(button->name, &text_metrics); + long width = text_metrics.width; + long height = text_metrics.actualBoundingBoxAscent - text_metrics.actualBoundingBoxDescent; + button->text_x = (button->w - width)/2; + button->text_y = (button->h + height)/2; + printf("%s: %ld, %ld; %ld, %ld\n", button->name, width, height, button->text_x, button->text_y); + } + this->canvas.setFillStyleRGB(0xFFFFFF); + this->canvas.fillText(button->name, button->x + button->text_x, button->y + button->text_y); + } +} + +void Menu::updateButtonSelections(long x, long y) { + for (LinkedList* node = this->buttons.root; node; node = node->next) { + MenuButton *button = &(node->val); + button->selected = x >= button->x && x <= button->x+button->w + && y >= button->y && y <= button->y+button->h; + + } +} + +const colorRGB_t s_pong_border_color = 0x2b8fbd; +const colorRGB_t s_pong_background_color = 0xFFFFFF; +const colorRGB_t s_pong_paddle_color = 0xFF0000; +const colorRGB_t s_pong_ball_color = 0x00FF00; + +extern "C" { + __attribute__((import_name("setElementText"))) + void set_element_text(const char* element_id, const char* text); +} +void Menu::tryButtonPress(long x, long y) { + this->updateButtonSelections(x, y); + for (LinkedList* node = this->buttons.root; node; node = node->next) { + MenuButton *button = &(node->val); + if (button->selected) { + switch (button->id) { + case 0: + this->state = MenuState::SinglePlayerPong; + this->s_pong = Pong(600, 600, s_pong_border_color, s_pong_background_color, s_pong_paddle_color, s_pong_ball_color); + set_element_text("control_text", "Move the paddle using a and d or the left and right arrow keys."); + break; + + case 1: + this->state = MenuState::TwoPlayerPong; + this->t_pong = TwoPlayerPong(this->width, this->height, true); + set_element_text("control_text", "Move the paddle using w and s or the up and down arrow keys."); + break; + + case 2: + this->state = MenuState::TwoPlayerPong; + this->t_pong = TwoPlayerPong(this->width, this->height, false); + set_element_text("control_text", "Move the left paddle using w and s. Move the right one with the up and down arrow keys."); + break; + + default: + break; + } + return; + } + } + + this->s_pong = Pong(width, height, s_pong_border_color, s_pong_background_color, s_pong_paddle_color, s_pong_ball_color); +} \ No newline at end of file diff --git a/examples/pong/pong-menu.h b/examples/pong/pong-menu.h new file mode 100644 index 00000000..f33b2031 --- /dev/null +++ b/examples/pong/pong-menu.h @@ -0,0 +1,69 @@ +// #ifndef PONG_MENU +// #define PONG_MENU +#include "canvas.h" +#include "pong.h" +#include "two-player-pong.h" + +template +struct LinkedList { + T val; + LinkedList* next; +}; + +template +class LinkedListRoot { + public: + LinkedList* root = NULL; + LinkedListRoot(); + // ~LinkedListRoot(); + void addNode(T val); + + private: + LinkedList* tail = NULL; + +}; + + +struct MenuButton { + long x, y; + long w, h; + const char* name; + int id; + bool selected, initialized; + long text_x, text_y; +}; + +enum class MenuState { + Menu, + SinglePlayerPong, + TwoPlayerPong +}; +class Menu { + public: + Menu(); + void setBounds(long width, long height); + + void mouseMoveEvent(long x, long y); + void mousePressEvent(long x, long y); + void keyDownEvent(long keycode); + void keyUpEvent(long keycode); + void render(long delta); + + private: + twrCanvas canvas; + long width; + long height; + LinkedListRoot buttons; + MenuState state = MenuState::Menu; + + void addButton(long x, long y, long w, long h, const char* name, int id); + void renderButtons(); + void tryButtonPress(long x, long y); + void updateButtonSelections(long x, long y); + + Pong s_pong; + TwoPlayerPong t_pong; + +}; + +// #endif \ No newline at end of file diff --git a/examples/pong/pong.cpp b/examples/pong/pong.cpp index 32a2562a..568d8a31 100644 --- a/examples/pong/pong.cpp +++ b/examples/pong/pong.cpp @@ -1,87 +1,11 @@ -#include -#include -#include -#include +#include "pong.h" +#include "twr-audio.h" -#include "canvas.h" +#include "extra.h" #define M_PI 3.14159265358979323846 -enum class PaddleDirection { - LEFT, - STATIONARY, - RIGHT -}; -class Pong { - public: - Pong(double width, double height, colorRGB_t border_color, colorRGB_t background_color, colorRGB_t paddle_color, colorRGB_t ball_color); - - void render(); - void tick(long time); - - void pressedLeft(); - void releasedLeft(); - void pressedRight(); - void releasedRight(); - - void pressedEnter(); - - private: - const double border_width = 10.0; - const double paddle_offset = 50; - const double paddle_height = 20; - const double paddle_width = 75; - const double ball_size = 20; - - #ifdef ASYNC - const long background_image_id = 1; - #endif - const double score_green_time = 500; - - const double paddle_speed = 100/1000.0; - - double width; - double height; - colorRGB_t border_color; - colorRGB_t background_color; - colorRGB_t paddle_color; - colorRGB_t ball_color; - twrCanvas canvas; - - double ball_velocity_x; - double ball_velocity_y; - //all coordinates are from the top left corner - double ball_x; - double ball_y; - double paddle_x; - PaddleDirection paddle_dir = PaddleDirection::STATIONARY; - //long last_paddle_press = 0; - bool left_pressed = false; - bool right_pressed = false; - - long last_timestamp = 0; - long run_time = 0; - int score = 0; - long score_time = 0; - bool game_running = true; - - void renderBackground(); - void renderBorder(); - void renderPaddle(); - void renderBall(); - void renderStats(); - - void tickPaddle(long time); - void tickBall(long time); - - void resetGame(); - void endGame(); - void renderEndGame(); - - void fillBorderedText(const char* text, double x, double y, double outer_width); - void strokeBorderedText(const char* text, double x, double y, double outer_width); -}; void Pong::endGame() { this->game_running = false; } @@ -92,7 +16,7 @@ void Pong::resetGame() { this->paddle_x = this->width/2.0 - this->paddle_width/2.0; const double start_speed = 200.0/1000.0; - double start_dir = rand()%360; + double start_dir = rand()%90 - 90; double start_dir_rad = start_dir * M_PI/180; this->ball_velocity_x = start_speed*cos(start_dir_rad); @@ -102,6 +26,8 @@ void Pong::resetGame() { this->score = 0; this->last_timestamp = 0; } +Pong::Pong() {} + Pong::Pong(double width, double height, colorRGB_t border_color, colorRGB_t background_color, colorRGB_t paddle_color, colorRGB_t ball_color) { this->width = width; this->height = height; @@ -110,30 +36,35 @@ Pong::Pong(double width, double height, colorRGB_t border_color, colorRGB_t back this->paddle_color = paddle_color; this->ball_color = ball_color; + this->bounce_noise = load_square_wave(493.883, 0.05, 48000); + this->lose_noise = load_square_wave(440, 0.25, 48000); + #ifdef ASYNC bool image_loaded = d2d_load_image("https://images.pexels.com/photos/235985/pexels-photo-235985.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1", background_image_id); assert(image_loaded); #endif //initialized random number generator - srand(time(NULL)); - - this->canvas.startDrawSequence(); + + srand(twr_epoch_timems()); - unsigned long img_buffer_len = this->canvas.getImageDataSize(this->width, this->height); - char* img_buffer = (char*)malloc(sizeof(char) * img_buffer_len); - this->canvas.getImageData(0.0, 0.0, this->width, this->height, (void*)img_buffer, img_buffer_len); + this->resetGame(); +} +Pong& Pong::operator=(const Pong& copy) { + this->width = copy.width; + this->height = copy.height; + this->border_color = copy.border_color; + this->background_color = copy.background_color; + this->paddle_color = copy.paddle_color; + this->ball_color = copy.ball_color; - long failed = 0; - for (int i = 0; i < img_buffer_len; i++) { - if (img_buffer[i] != (char)0x00) { - failed++; - } - } - assert(failed == 0); - this->canvas.endDrawSequence(); + this->bounce_noise = copy.bounce_noise; + this->lose_noise = copy.lose_noise; this->resetGame(); + + return *this; } + void Pong::render() { this->canvas.startDrawSequence(); this->canvas.reset(); @@ -152,98 +83,80 @@ void Pong::render() { this->canvas.endDrawSequence(); } void Pong::renderBackground() { - #ifdef ASYNC - this->canvas.drawImage(background_image_id, 0, 0); - #endif - - //just used for testing getLineDash - //not recommended for your main render loop - unsigned long segment_len1 = this->canvas.getLineDashLength(); - double* buffer = NULL; - if (segment_len1 > 0) { - buffer = (double*)malloc(sizeof(double)*segment_len1); - } - double true_len = this->canvas.getLineDash(segment_len1, buffer); - assert(true_len == segment_len1); - - this->canvas.beginPath(); - const long segment_len2 = 1; - double segments[segment_len2] = {20}; - this->canvas.setLineDash(segment_len2, segments); - this->canvas.setLineWidth(10.0); - const long offset_max = 40; - const long offset_divisor = 10; - if (this->ball_velocity_x > 0) { - this->canvas.setLineDashOffset(offset_max - (this->last_timestamp/offset_divisor)%offset_max); - } else { - this->canvas.setLineDashOffset((this->last_timestamp/offset_divisor)%offset_max); - } - - this->canvas.setStrokeStyleRGB(0xE0E0E0); - this->canvas.moveTo(0.0, this->height); - this->canvas.quadraticCurveTo(this->height/2.0, this->width - this->paddle_offset, this->width, this->height); - this->canvas.stroke(); - this->canvas.setLineDashOffset(0.0); - - this->canvas.setLineDash(segment_len1, buffer); - if (buffer) - free(buffer); - - this->canvas.beginPath(); - this->canvas.setStrokeStyleRGBA(0xE0E0E040); - this->canvas.moveTo(0, 0); - const double p1_x = this->width/2.0; - const double p1_y = this->height/4.0 * 3.0; - const double radius = 360.0; - this->canvas.arcTo(p1_x, p1_y, 0.0, this->height, radius); - this->canvas.moveTo(this->width, 0.0); - this->canvas.arcTo(p1_x, p1_y, this->width, this->height, radius); - this->canvas.stroke(); + #ifdef ASYNC + this->canvas.drawImage(background_image_id, 0, 0, this->width, this->height, 0, 0, 0, 0); + #endif + + + this->canvas.save(); + + this->canvas.beginPath(); + const long segment_len2 = 1; + double segments[segment_len2] = {20}; + this->canvas.setLineDash(segment_len2, segments); + this->canvas.setLineWidth(10.0); + const long offset_max = 40; + const long offset_divisor = 10; + if (this->ball_velocity_x > 0) { + this->canvas.setLineDashOffset(offset_max - (this->last_timestamp/offset_divisor)%offset_max); + } else { + this->canvas.setLineDashOffset((this->last_timestamp/offset_divisor)%offset_max); + } + + this->canvas.setStrokeStyleRGB(0xE0E0E0); + this->canvas.moveTo(0.0, this->height); + this->canvas.quadraticCurveTo(this->height/2.0, this->width - this->paddle_offset, this->width, this->height); + this->canvas.stroke(); + + this->canvas.restore(); + + this->canvas.beginPath(); + this->canvas.setStrokeStyleRGBA(0xE0E0E040); + this->canvas.moveTo(0, 0); + const double p1_x = this->width/2.0; + const double p1_y = this->height/4.0 * 3.0; + const double radius = 360.0; + this->canvas.arcTo(p1_x, p1_y, 0.0, this->height, radius); + this->canvas.moveTo(this->width, 0.0); + this->canvas.arcTo(p1_x, p1_y, this->width, this->height, radius); + this->canvas.stroke(); } void Pong::renderBorder() { - this->canvas.setLineWidth(this->border_width); - double offset = this->border_width/2.0; - - //clear anything on the outer edges of the rounded corners - this->canvas.setStrokeStyleRGB(this->background_color); - this->canvas.strokeRect(offset - 1, offset - 1, this->width - this->border_width + 2, this->height - this->border_width + 2); + this->canvas.setLineWidth(this->border_width); + double offset = this->border_width/2.0; + + //clear anything on the outer edges of the rounded corners + this->canvas.setStrokeStyleRGB(this->background_color); + this->canvas.strokeRect(offset - 1, offset - 1, this->width - this->border_width + 2, this->height - this->border_width + 2); + //clear everything outside to the right of the border (for when canvas is larger than play area) + this->canvas.setFillStyleRGB(this->background_color); + this->canvas.fillRect(this->width, 0.0, 200.0, this->height); + + this->canvas.setStrokeStyleRGB(this->border_color); + this->canvas.beginPath(); + this->canvas.roundRect(offset, offset, this->width - this->border_width, this->height - this->border_width, 20.0); + this->canvas.stroke(); - this->canvas.setStrokeStyleRGB(this->border_color); - this->canvas.beginPath(); - this->canvas.roundRect(offset, offset, this->width - this->border_width, this->height - this->border_width, 20.0); - this->canvas.stroke(); } void Pong::renderBall() { - //start transform used to revert back to original state - //mainly used for testing getTransform as it flushes the instruction buffer - // and may cause a drop in performance because of it - d2d_2d_matrix start_transform; - this->canvas.getTransform(&start_transform); - - // this->canvas.translate(this->ball_x, this->ball_y); - this->canvas.transform(1.0, 0.0, 0.0, 1.0, this->ball_x, this->ball_y); - - this->canvas.setFillStyleRGB(this->ball_color); - this->canvas.setLineWidth(2.0); - this->canvas.beginPath(); - this->canvas.moveTo(this->ball_size/2.0, 0); - this->canvas.lineTo(this->ball_size, this->ball_size); - this->canvas.lineTo(0, this->ball_size); - //this->canvas.lineTo(this->ball_x + this->ball_size/2.0, this->ball_y); - this->canvas.closePath(); - this->canvas.fill(); - double mid_height_offset = this->ball_size/2.0; - double slope = 2; //rises ball_width, runs 1/2 ball_width - double mid_width_offset = (1/slope) * mid_height_offset; + this->canvas.translate(this->ball_x, this->ball_y); - double angle = 45 * M_PI/180; - this->canvas.translate(this->ball_size/2.0, this->ball_size/4.0 * 3); - this->canvas.rotate(angle); + this->canvas.setFillStyleRGB(this->ball_color); + this->canvas.setLineWidth(2.0); + this->canvas.beginPath(); + this->canvas.moveTo(this->ball_size/2.0, 0); + this->canvas.lineTo(this->ball_size, this->ball_size); + this->canvas.lineTo(0, this->ball_size); + //this->canvas.lineTo(this->ball_x + this->ball_size/2.0, this->ball_y); + this->canvas.closePath(); + this->canvas.fill(); - this->canvas.clearRect(-mid_width_offset, -mid_height_offset/2.0, mid_width_offset*2.0, mid_height_offset); + double angle = 45 * M_PI/180; + this->canvas.translate(this->ball_size/2.0, this->ball_size/4.0 * 3); + this->canvas.rotate(angle); - this->canvas.setTransform(&start_transform); + this->canvas.resetTransform(); } void Pong::renderPaddle() { this->canvas.setFillStyleRGB(this->paddle_color); @@ -262,13 +175,11 @@ void Pong::renderStats() { const char * stat_font = "20px serif"; this->canvas.setFont(stat_font); #ifdef ASYNC - this->canvas.setStrokeStyleRGB(0xededed); + this->canvas.setFillStyleRGB(0xededed); #else - this->canvas.setStrokeStyleRGB(0x000000); + this->canvas.setFillStyleRGB(0x000000); #endif - // this->canvas.setFillStyleRGB(0x000000); - this->canvas.strokeText(text, 30.0, 30.0); - // this->strokeBorderedText(text, 30.0, 30.0, 4.0); + this->canvas.fillText(text, 30.0, 30.0); long minutes = (this->run_time/1000)/60; long seconds = (this->run_time/1000)%60; @@ -276,7 +187,7 @@ void Pong::renderStats() { const int time_len = 14; char time[score_len] = {0}; snprintf(time, time_len-1, "Time: %02ld:%02ld", minutes, seconds); - this->canvas.strokeText(time, this->width - 150.0, 30.0); + this->canvas.fillText(time, this->width - 150.0, 30.0); // this->strokeBorderedText(text, this->width - 150.0, 30.0, 2.0); this->canvas.beginPath(); @@ -351,6 +262,7 @@ void Pong::tick(long time) { this->run_time += delta; } +const double BALL_BOUNCE_VOL = 2.0; void Pong::tickBall(long delta) { double prev_y = this->ball_y; this->ball_x += this->ball_velocity_x * delta; @@ -360,17 +272,21 @@ void Pong::tickBall(long delta) { if (this->ball_x <= this->border_width) { //left wall this->ball_x = this->border_width; this->ball_velocity_x *= -1; + twr_audio_play_volume(this->bounce_noise, BALL_BOUNCE_VOL, 0.0); } else if (this->ball_x >= this->width - this->ball_size - this->border_width) { //right wall this->ball_x = this->width - this->ball_size - this->border_width; this->ball_velocity_x *= -1; + twr_audio_play_volume(this->bounce_noise, BALL_BOUNCE_VOL, 0.0); } //x and y are seperate checks for the corner case if (this->ball_y <= border_width) { //top wall this->ball_y = this->border_width; this->ball_velocity_y *= -1; + twr_audio_play_volume(this->bounce_noise, BALL_BOUNCE_VOL, 0.0); } else if (this->ball_y >= this->height - this->ball_size - this->border_width) { //bottom wall, lost game this->ball_y = this->height - this->ball_size - this->border_width - 1.0; + twr_audio_play(this->lose_noise); this->endGame(); } @@ -410,6 +326,7 @@ void Pong::tickBall(long delta) { //set score time this->score_time = this->last_timestamp; } + twr_audio_play_volume(this->bounce_noise, BALL_BOUNCE_VOL, 0.0); } } void Pong::tickPaddle(long delta) { @@ -473,51 +390,50 @@ void Pong::strokeBorderedText(const char* text, double x, double y, double outer this->canvas.restore(); this->canvas.strokeText(text, x, y); } -double width = 600.0; -double height = 600.0; - -colorRGB_t border_color = 0x2b8fbd; -colorRGB_t background_color = 0xFFFFFF; -colorRGB_t paddle_color = 0xFF0000; -colorRGB_t ball_color = 0x00FF00; -Pong game(width, height, border_color, background_color, paddle_color, ball_color); -extern "C" void render() { - game.render(); -} -extern "C" void tick(long time) { - game.tick(time); -} + enum class KeyCode { - ArrowLeft = 37, - ArrowRight = 39, - Enter = 13 + ArrowLeft = 8592, + ArrowRight = 8594, + a = 97, + d = 100, + enter = 10, }; -extern "C" void keyEvent(bool released, int key) { - switch ((KeyCode)key) { - case KeyCode::ArrowLeft: - { - if (released) { - game.releasedLeft(); - } else { - game.pressedLeft(); - } - } - break; - - case KeyCode::ArrowRight: - { - if (released) { - game.releasedRight(); - } else { - game.pressedRight(); - } - } - break; - case KeyCode::Enter: - { - game.pressedEnter(); - } - break; - } -} \ No newline at end of file +void Pong::keyDownEvent(long keycode) { + switch ((KeyCode)keycode) { + case KeyCode::ArrowLeft: + case KeyCode::a: + this->pressedLeft(); + break; + + case KeyCode::ArrowRight: + case KeyCode::d: + this->pressedRight(); + break; + + case KeyCode::enter: + this->pressedEnter(); + break; + + default: + //do nothing + break; + } +} +void Pong::keyUpEvent(long keycode) { + switch ((KeyCode)keycode) { + case KeyCode::ArrowLeft: + case KeyCode::a: + this->releasedLeft(); + break; + + case KeyCode::ArrowRight: + case KeyCode::d: + this->releasedRight(); + break; + + default: + //do nothing + break; + } +} diff --git a/examples/pong/pong.h b/examples/pong/pong.h new file mode 100644 index 00000000..eb1f0cf9 --- /dev/null +++ b/examples/pong/pong.h @@ -0,0 +1,90 @@ +#include +#include +#include +#include + +#include "canvas.h" + +enum class PaddleDirection { + LEFT, + STATIONARY, + RIGHT +}; +class Pong { + public: + Pong(double width, double height, colorRGB_t border_color, colorRGB_t background_color, colorRGB_t paddle_color, colorRGB_t ball_color); + Pong(); + Pong& operator=(const Pong& copy); + + void render(); + void tick(long time); + + void pressedLeft(); + void releasedLeft(); + void pressedRight(); + void releasedRight(); + + void pressedEnter(); + + void keyDownEvent(long keycode); + void keyUpEvent(long keycode); + + private: + const double border_width = 10.0; + const double paddle_offset = 50; + const double paddle_height = 20; + const double paddle_width = 75; + const double ball_size = 20; + + #ifdef ASYNC + const long background_image_id = 1; + #endif + + const double score_green_time = 500; + + const double paddle_speed = 100/1000.0; + + double width; + double height; + colorRGB_t border_color; + colorRGB_t background_color; + colorRGB_t paddle_color; + colorRGB_t ball_color; + twrCanvas canvas; + + double ball_velocity_x; + double ball_velocity_y; + //all coordinates are from the top left corner + double ball_x; + double ball_y; + double paddle_x; + PaddleDirection paddle_dir = PaddleDirection::STATIONARY; + //long last_paddle_press = 0; + bool left_pressed = false; + bool right_pressed = false; + + long last_timestamp = 0; + long run_time = 0; + int score = 0; + long score_time = 0; + bool game_running = true; + + long bounce_noise; + long lose_noise; + + void renderBackground(); + void renderBorder(); + void renderPaddle(); + void renderBall(); + void renderStats(); + + void tickPaddle(long time); + void tickBall(long time); + + void resetGame(); + void endGame(); + void renderEndGame(); + + void fillBorderedText(const char* text, double x, double y, double outer_width); + void strokeBorderedText(const char* text, double x, double y, double outer_width); +}; \ No newline at end of file diff --git a/examples/pong/tsconfig.json b/examples/pong/tsconfig.json new file mode 100644 index 00000000..b2f64c34 --- /dev/null +++ b/examples/pong/tsconfig.json @@ -0,0 +1,36 @@ + { + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + "incremental": false, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + "composite": false, /* Enable constraints that allow a TypeScript project to be used with project references. */ + + /* Language and Environment */ + "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": ["ES2023","DOM"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + + /* Modules */ + "module": "ESNext", /* Specify what module code is generated. */ + "moduleResolution": "Bundler", + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "declarationMap": true, /* Create sourcemaps for d.ts files. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + "outDir": "./out", /* Specify an output folder for all emitted files. */ + + /* Interop Constraints */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "paths": { + "twr-wasm": ["./../../lib-js/index"] + } + }, + "files": ["jsEventsLib.ts"] + } + + + \ No newline at end of file diff --git a/examples/pong/two-player-pong.cpp b/examples/pong/two-player-pong.cpp new file mode 100644 index 00000000..5c3f80bc --- /dev/null +++ b/examples/pong/two-player-pong.cpp @@ -0,0 +1,523 @@ +#include "two-player-pong.h" +#include +#include +#include +#include "twr-audio.h" + +#include "extra.h" + +#define M_PI 3.14159265358979323846 + +const double BALL_WIDTH = 25.0; +const double BALL_HEIGHT = 25.0; + +const double PADDLE_HEIGHT = 100.0; +const double PADDLE_WIDTH = 20.0; +const double PADDLE_OFFSET = 10.0; +const double PADDLE_SPEED = 150.0; + +const colorRGB_t BALL_COLOR = 0xFFFFFF; +const colorRGB_t PADDLE_ONE_COLOR = 0xFFFFFF; +const colorRGB_t PADDLE_TWO_COLOR = 0xFFFFFF; + + + + +TwoPlayerPong::TwoPlayerPong() { + this->width = 0.0; + this->height = 0.0; + this->hasAI = false; +} + +TwoPlayerPong::TwoPlayerPong(double width, double height, bool hasAI) { + this->width = width; + this->height = height; + this->hasAI = hasAI; + this->bounce_noise = load_square_wave(493.883, 0.05, 48000); + this->score_noise = load_square_wave(440, 0.05, 48000); + srand(time(NULL)); + + this->resetGame(); +} + +TwoPlayerPong& TwoPlayerPong::operator=(const TwoPlayerPong& copy) { + this->width = copy.width; + this->height = copy.height; + this->hasAI = copy.hasAI; + this->bounce_noise = copy.bounce_noise; + this->score_noise = copy.score_noise; + + this->resetGame(); + + return *this; +} + + +void TwoPlayerPong::resetBall() { + this->ball.x = (this->width - BALL_WIDTH)/2.0; + this->ball.y = (this->height - BALL_HEIGHT)/2.0; + + const double start_speed = 200.0; + //only allow ball to spawn in 60 degree cone in either direction + double start_part = rand()%60; + double dir = rand()%2; //o or 1; 0 is right, 1 is left + double start_dir = (180 - start_part) - 180*dir; + double start_dir_rad = start_dir * M_PI/180; + + this->ball.v_x = start_speed*cos(start_dir_rad); + this->ball.v_y = start_speed*sin(start_dir_rad); +} +void TwoPlayerPong::resetGame() { + this->resetBall(); + + this->paddleOne.dir = PaddleDir::STILL; + this->paddleOne.y = (this->height - PADDLE_HEIGHT)/2.0; + + this->paddleTwo.dir = PaddleDir::STILL; + this->paddleTwo.y = (this->height - PADDLE_HEIGHT)/2.0; + + this->last_time = -1.0; + + this->stats.l_score = 0; + this->stats.r_score = 0; + + running = true; +} + + +void TwoPlayerPong::render() { + this->canvas.startDrawSequence(); + + this->canvas.reset(); + this->canvas.setFillStyleRGB(0x000000); + this->canvas.fillRect(0, 0, this->width, this->height); + + this->renderStats(); + this->renderBackground(); + this->renderBall(); + this->renderPaddles(); + + + if (!this->running) { + this->renderWinScreen(); + } + + this->canvas.endDrawSequence(); +} + +void TwoPlayerPong::renderStats() { + + this->canvas.setFillStyleRGBA(0xFFFFFFA0); + this->canvas.setFont("25px Seriph"); + + if (!this->stats.intialized) { + const double x_off = 20.0; + const double y_off = 20.0; + + d2d_text_metrics l_score; + this->canvas.measureText("0000", &l_score); + + this->stats.l_score_pos.x = x_off; + this->stats.l_score_pos.y = y_off + l_score.actualBoundingBoxAscent - l_score.actualBoundingBoxDescent; + + d2d_text_metrics r_score; + this->canvas.measureText("0000", &r_score); + + this->stats.r_score_pos.x = this->width - r_score.width - x_off; + this->stats.r_score_pos.y = y_off + r_score.actualBoundingBoxAscent - r_score.actualBoundingBoxDescent; + + + // printf("hi!!! %f, %f\n", this->stats.score_x, this->stats.score_y); + this->stats.intialized = true; + } + + const int score_len = 20; + char l_score_str[score_len]; + + snprintf(l_score_str, score_len-1, "% 4ld", this->stats.l_score); + this->canvas.fillText(l_score_str, this->stats.l_score_pos.x, this->stats.l_score_pos.y); + + + char r_score_str[score_len]; + + snprintf(r_score_str, score_len-1, "%ld", this->stats.r_score); + this->canvas.fillText(r_score_str, this->stats.r_score_pos.x, this->stats.r_score_pos.y); + + +} + + +void TwoPlayerPong::renderBall() { + this->canvas.setFillStyleRGB(BALL_COLOR); + this->canvas.fillRect(this->ball.x, this->ball.y, BALL_WIDTH, BALL_HEIGHT); +} + +void TwoPlayerPong::renderPaddles() { + this->canvas.setFillStyleRGB(PADDLE_ONE_COLOR); + this->canvas.fillRect(PADDLE_OFFSET, this->paddleOne.y, PADDLE_WIDTH, PADDLE_HEIGHT); + + this->canvas.setFillStyleRGB(PADDLE_TWO_COLOR); + this->canvas.fillRect(this->width - PADDLE_OFFSET - PADDLE_WIDTH, this->paddleTwo.y, PADDLE_WIDTH, PADDLE_HEIGHT); + +} + +void TwoPlayerPong::renderBackground() { + // this->canvas.setFillStyleRGB(0x0FF00); + // this->canvas.fillRect(0, this->height/2.0 - 5.0, this->width, 10.0); + this->canvas.setStrokeStyleRGBA(0xFFFFFFA0); + + this->canvas.setLineWidth(10.0); + + const long NUM_DASHES = 15; + double dash_len = this->height/(NUM_DASHES * 2.0); + + this->canvas.setLineDash(1, &dash_len); + this->canvas.setLineDashOffset(dash_len/2.0); + + this->canvas.beginPath(); + this->canvas.moveTo(this->width/2.0, 0.0); + this->canvas.lineTo(this->width/2.0, this->height); + this->canvas.stroke(); +} + +void TwoPlayerPong::tick(long time) { + double s_time = (double)time/1000.0; //converting time to seconds + double s_delta = last_time < 0 ? 0 : (double)s_time - last_time; //getting delta in seconds + last_time = s_time; //setting last time to current one + + if (!running) { + return; + } + + this->updatePaddles(s_delta); + this->updateBall(s_delta); + this->updateAI(); + +} + +void TwoPlayerPong::updateAI() { + if (!this->hasAI) return; + + double center_y = this->paddleTwo.y + PADDLE_HEIGHT/2.0; + + if (center_y > this->ball.y) { + this->paddleTwo.dir = PaddleDir::UP; + } else if (center_y < this->ball.y) { + this->paddleTwo.dir = PaddleDir::DOWN; + } else { + this->paddleTwo.dir = PaddleDir::STILL; + } +} + + +void updatePaddle(Paddle& paddle, double height, double delta) { + switch (paddle.dir) { + case PaddleDir::UP: + { + if (paddle.y > 0) { + paddle.y -= PADDLE_SPEED * delta; + if (paddle.y < 0) paddle.y = 0; + } + } + break; + + case PaddleDir::DOWN: + { + double max_y = height - PADDLE_HEIGHT; + if (paddle.y < max_y) { + paddle.y += PADDLE_SPEED * delta; + if (paddle.y > max_y) paddle.y = max_y; + } + } + break; + + default: + break; + } +} +void TwoPlayerPong::updatePaddles(double delta) { + updatePaddle(this->paddleOne, this->height, delta); + updatePaddle(this->paddleTwo, this->height, delta); +} + +enum class KeyCode { + ArrowUp = 8593, + ArrowDown = 8595, + w = 119, + s = 115, + enter = 10, +}; + +void TwoPlayerPong::keyDownEvent(long keycode) { + switch ((KeyCode)keycode) { + case KeyCode::ArrowUp: + if (this->hasAI) { + paddleOne.dir = PaddleDir::UP; + } else { + paddleTwo.dir = PaddleDir::UP; + } + break; + + case KeyCode::ArrowDown: + if (this->hasAI) { + paddleOne.dir = PaddleDir::DOWN; + } else { + paddleTwo.dir = PaddleDir::DOWN; + } + break; + + case KeyCode::w: + paddleOne.dir = PaddleDir::UP; + break; + + case KeyCode::s: + paddleOne.dir = PaddleDir::DOWN; + break; + + case KeyCode::enter: + if(!this->running) + this->resetGame(); + + default: + break; + } +} + +void unpressPaddleKey(Paddle &paddle, PaddleDir dir) { + if (paddle.dir == dir) { + paddle.dir = PaddleDir::STILL; + } +} +void TwoPlayerPong::keyUpEvent(long keycode) { + switch ((KeyCode)keycode) { + case KeyCode::ArrowUp: + if (this->hasAI) { + unpressPaddleKey(this->paddleOne, PaddleDir::UP); + } else { + unpressPaddleKey(this->paddleTwo, PaddleDir::UP); + } + break; + + case KeyCode::ArrowDown: + if (this->hasAI) { + unpressPaddleKey(this->paddleOne, PaddleDir::DOWN); + } else { + unpressPaddleKey(this->paddleTwo, PaddleDir::DOWN); + } + break; + + case KeyCode::w: + unpressPaddleKey(this->paddleOne, PaddleDir::UP); + break; + + case KeyCode::s: + unpressPaddleKey(this->paddleOne, PaddleDir::DOWN); + break; + + default: + break; + } +} + +double get_paddle_vel(Paddle &paddle) { + switch (paddle.dir) { + case PaddleDir::UP: + return -PADDLE_SPEED*0.1; + case PaddleDir::DOWN: + return PADDLE_SPEED*0.1; + case PaddleDir::STILL: + return 0.0; + } +} + +template +T better_abs(T val) { + if (val < 0) { + return -val; + } else { + return val; + } +} + + +const double BALL_BOUNCE_VOL = 2.0; +void paddleCollision(Ball& ball, double& n_x, double& n_y, Paddle& paddle, double paddle_x, long bounce_noise){ + + double paddle_middle = paddle.y + PADDLE_HEIGHT/2.0; + double ball_middle = ball.y + BALL_HEIGHT/2.0; + + double paddle_middle_x = paddle_x + PADDLE_WIDTH/2.0; + double ball_middle_x = ball.x + BALL_WIDTH/2.0; + + if ( + paddle_x <= n_x + BALL_HEIGHT + && paddle_x + PADDLE_WIDTH >= n_x + && paddle.y <= n_y + BALL_HEIGHT + && paddle.y + PADDLE_HEIGHT >= n_y + ) { + if (ball.x + BALL_WIDTH <= paddle_x) { //hit left side + ball.v_x = -better_abs(ball.v_x); + n_x = paddle_x - BALL_WIDTH; + } else if (ball.x >= paddle_x + PADDLE_WIDTH) { //hit right side + ball.v_x = better_abs(ball.v_x); + n_x = paddle_x + PADDLE_WIDTH; + } else if (ball_middle - paddle_middle < 0) { //hit the top + n_y = paddle.y - BALL_HEIGHT; + ball.v_y = -better_abs(ball.v_y); + } else { //hit the bottom + n_y = paddle.y + PADDLE_HEIGHT; + ball.v_y = better_abs(ball.v_y); + } + + twr_audio_play_volume(bounce_noise, BALL_BOUNCE_VOL, 0.0); + + //add paddle velocity to ball + double paddle_vel = get_paddle_vel(paddle); + ball.v_y += paddle_vel; + + //reflect at an angle represented by circle around paddle + double paddle_angle = atan2(ball_middle - paddle_middle, ball_middle_x - paddle_middle_x); + + double ball_angle = atan2(ball.v_y, ball.v_x); + + double ball_speed = sqrt(ball.v_x*ball.v_x + ball.v_y*ball.v_y); + + double new_angle = ball_angle + (paddle_angle - ball_angle) * 0.01; + + ball.v_x = ball_speed * cos(new_angle); + ball.v_y = ball_speed * sin(new_angle); + + } +} +void TwoPlayerPong::updateBall(double delta) { + double n_x = this->ball.x + this->ball.v_x*delta; + double n_y = this->ball.y + this->ball.v_y*delta; + + double max_x = this->width - BALL_WIDTH; + double max_y = this->height - BALL_HEIGHT; + + if (n_y < 0) { + //bounce off top wall by flipping y direction + //interpolate to add the extra y back in th eopposite direction + n_y = 0 - n_y; + this->ball.v_y *= -1; + twr_audio_play_volume(this->bounce_noise, BALL_BOUNCE_VOL, 0.0); + } else if (n_y > max_y) { + //bounce off bottom wall by flipping y direction + //interpolate to add the extra y back in the opposite direction + + //max_y - (n_y - max_y) = max_y + max_y - n_y = 2*max_y - n_y + n_y = 2*max_y - n_y; + this->ball.v_y *= -1; + twr_audio_play_volume(this->bounce_noise, BALL_BOUNCE_VOL, 0.0); + } + + if (n_x < 0) { //hit left + this->ballScored(false); + twr_audio_play(this->score_noise); + return; + } else if (n_x > max_x) { + this->ballScored(true); + twr_audio_play(this->score_noise); + return; + } + + + //left paddle + paddleCollision(this->ball, n_x, n_y, paddleOne, PADDLE_OFFSET, this->bounce_noise); + + //right paddle + paddleCollision(this->ball, n_x, n_y, paddleTwo, this->width - PADDLE_OFFSET - PADDLE_WIDTH, this->bounce_noise); + + this->ball.x = n_x; + this->ball.y = n_y; + + // printf("%f, %f\n", this->ball.x, this->ball.y); +} + +void TwoPlayerPong::ballScored(bool right) { + int changed_score = 0; + if (right) { + this->stats.l_score += 1; + changed_score = this->stats.l_score; + } else { + this->stats.r_score += 1; + changed_score = this->stats.r_score; + } + + const int WINNING_SCORE = 10; + + if (changed_score >= WINNING_SCORE) { + this->running = false; + } else { + this->resetBall(); + } +} + +void fillBorderedText(twrCanvas & canvas, const char* text, double x, double y, double outer_width) { + canvas.save(); + canvas.setLineWidth(outer_width); + canvas.setStrokeStyleRGB(0xFFFFFF); + canvas.strokeText(text, x, y); + + canvas.restore(); + canvas.fillText(text, x, y); +} + +void TwoPlayerPong::renderWinScreen() { + const double HEIGHT_DIST = 20.0; + + const char* RESET_STR = "Press Enter to Play Again"; + const int winner_len = 22; + char winner_str[winner_len] = {0}; + snprintf(winner_str, winner_len - 1, "%s is the winner!", this->stats.l_score < this->stats.r_score ? "Right" : "Left"); + + const char* RESET_FONT = "32px Seriph"; + const colorRGB_t RESET_COLOR = 0xFF0000FF; + + const char* WINNER_FONT = "48px Seriph"; + const colorRGB_t WINNER_COLOR = 0x00FF00FF; + + this->canvas.setLineDash(0, NULL); + + + if (!this->initialized_win) { + this->initialized_win = true; + + + d2d_text_metrics winner_met; + this->canvas.setFont(WINNER_FONT); + this->canvas.measureText(winner_str, &winner_met); + + this->winner_pos.x = (this->width - winner_met.width)/2.0; + double winner_height = winner_met.actualBoundingBoxAscent - winner_met.actualBoundingBoxDescent; + + d2d_text_metrics reset_met; + this->canvas.setFont(RESET_FONT); + this->canvas.measureText(RESET_STR, &reset_met); + + this->reset_pos.x = (this->width - reset_met.width)/2.0; + double reset_height = reset_met.actualBoundingBoxAscent - reset_met.actualBoundingBoxDescent; + + + double total_height = winner_height + HEIGHT_DIST + reset_height; + printf("%f, %f, %f, %f\n", winner_height, HEIGHT_DIST, reset_height, total_height); + + double height_offset = (this->height - total_height)/2.0; + + this->winner_pos.y = height_offset; + this->reset_pos.y = height_offset + winner_height + HEIGHT_DIST; + } + + // this->canvas.reset(); + this->canvas.setFont(WINNER_FONT); + this->canvas.setFillStyleRGBA(WINNER_COLOR); + // this->canvas.fillText(winner_str, this->winner_pos.x, this->winner_pos.y); + fillBorderedText(this->canvas, winner_str, this->winner_pos.x, this->winner_pos.y, 5.0); + + this->canvas.setFont(RESET_FONT); + this->canvas.setFillStyleRGBA(RESET_COLOR); + // this->canvas.fillText(RESET_STR, this->reset_pos.x, this->reset_pos.y); + fillBorderedText(this->canvas, RESET_STR, this->reset_pos.x, this->reset_pos.y, 3.0); + +} \ No newline at end of file diff --git a/examples/pong/two-player-pong.h b/examples/pong/two-player-pong.h new file mode 100644 index 00000000..490cd2a7 --- /dev/null +++ b/examples/pong/two-player-pong.h @@ -0,0 +1,90 @@ +#include + +enum class PaddleDir { + UP, + STILL, + DOWN +}; + +template +struct Vec2D { + T x, y; +}; + +struct Paddle { + PaddleDir dir; + double y; +}; + +struct Ball { + double x, y; + double v_x, v_y; +}; + +struct Stats { + bool intialized = false; + + long l_score = 0; + long r_score = 0; + + Vec2D l_score_pos; + Vec2D r_score_pos; +}; + +class TwoPlayerPong { + public: + TwoPlayerPong(); + TwoPlayerPong(double width, double height, bool hasAI); + TwoPlayerPong& operator=(const TwoPlayerPong& copy); + + void render(); + void tick(long time); + + void keyDownEvent(long keycode); + void keyUpEvent(long keycode); + + + private: + double width; + double height; + + bool hasAI; + + twrCanvas canvas; + + Paddle paddleOne; + Paddle paddleTwo; + Ball ball; + + Stats stats; + + double last_time = -1.0; + + bool running = true; + + bool initialized_win = false; + Vec2D winner_pos; + Vec2D reset_pos; + + long bounce_noise; + long score_noise; + + + + void renderBackground(); + void renderPaddles(); + void renderStats(); + void renderBall(); + void renderWinScreen(); + + void updatePaddles(double delta); + void updateBall(double delta); + + void updateAI(); + + void resetBall(); + void resetGame(); + + void ballScored(bool right); + +}; \ No newline at end of file diff --git a/examples/server.py b/examples/server.py index 5ba249b2..decb75e9 100644 --- a/examples/server.py +++ b/examples/server.py @@ -2,15 +2,19 @@ from http import server # Python 3 class MyHTTPRequestHandler(server.SimpleHTTPRequestHandler): - def end_headers(self): - self.send_my_headers() - server.SimpleHTTPRequestHandler.end_headers(self) + def end_headers(self): + self.send_my_headers() + server.SimpleHTTPRequestHandler.end_headers(self) - def send_my_headers(self): - self.send_header("Access-Control-Allow-Origin", "*") - self.send_header("Cross-Origin-Embedder-Policy", "require-corp") - self.send_header("Cross-Origin-Opener-Policy", "same-origin") + def send_my_headers(self): + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Cross-Origin-Embedder-Policy", "require-corp") + self.send_header("Cross-Origin-Opener-Policy", "same-origin") + # Turn off caching + self.send_header("Cache-Control", "no-cache, no-store, must-revalidate") + self.send_header("Pragma", "no-cache") + self.send_header("Expires", "0") if __name__ == '__main__': - server.test(HandlerClass=MyHTTPRequestHandler) - \ No newline at end of file + server.test(HandlerClass=MyHTTPRequestHandler) + diff --git a/examples/terminal/Makefile b/examples/terminal/Makefile index 06918ccf..2b96e229 100644 --- a/examples/terminal/Makefile +++ b/examples/terminal/Makefile @@ -21,11 +21,11 @@ CFLAGS_DEBUG := -c -Wall -g -O0 $(TWRCFLAGS) wasm: terminal.wasm terminal.o: terminal.c - $(CC) $(CFLAGS) $< -o $@ + $(CC) $(CFLAGS_DEBUG) $< -o $@ terminal.wasm: terminal.o # wasm-ld takes WebAssembly binaries as inputs and produces a WebAssembly binary as its output. Mimics behavior of the ELF lld. - wasm-ld terminal.o ../../lib-c/twr.a -o terminal.wasm \ + wasm-ld terminal.o ../../lib-c/twrd.a -o terminal.wasm \ --no-entry --shared-memory --no-check-features --initial-memory=1048576 --max-memory=1048576 \ --export=show_terminal diff --git a/examples/terminal/terminal.c b/examples/terminal/terminal.c index 7e6fb672..7b553893 100644 --- a/examples/terminal/terminal.c +++ b/examples/terminal/terminal.c @@ -4,13 +4,11 @@ #include #include "twr-crt.h" -/* this twr-wasm C example draws a utf-8 string in the middle of a windowed console, */ +/* this twr-wasm C example draws a utf-8 string in the middle of a terminal console, */ /* and allows the user to move the string up or down with the u, d or arrow keys */ -/* see include/twr-io.h for available functions to draw chars to windowed console */ - void draw_outline(twr_ioconsole_t* io); -void show_str_centered(twr_ioconsole_t* io, int h, const char* str, unsigned long f, unsigned long b); +void show_str_centered(twr_ioconsole_t* io, int y, const char* str, unsigned long forecolor, unsigned long backcolor); void show_terminal() { twr_ioconsole_t* io=twr_get_stdio_con(); diff --git a/examples/tests-audio/Makefile b/examples/tests-audio/Makefile new file mode 100644 index 00000000..7aeaa05c --- /dev/null +++ b/examples/tests-audio/Makefile @@ -0,0 +1,32 @@ +CC := clang +TWRCFLAGS := --target=wasm32 -nostdinc -nostdlib -isystem ../../include +CPPLIB := ../twr-cpp +CFLAGS := -c -Wall -O3 $(TWRCFLAGS) -I $(CPPLIB) +CFLAGS_DEBUG := -c -Wall -g -O0 $(TWRCFLAGS) -I $(CPPLIB) + + + +.PHONY: default + +default: tests-audio.wasm tests-audio-a.wasm clearIODiv.js + +clearIODiv.js: index.html clearIODiv.ts + tsc + +tests-audio.o: tests-audio.c + $(CC) $(CFLAGS) $< -o $@ + +tests-audio-a.o: tests-audio.c + $(CC) $(CFLAGS) $< -o $@ -DASYNC + +tests-audio.wasm: tests-audio.o + wasm-ld tests-audio.o ../../lib-c/twr.a -o tests-audio.wasm \ + --no-entry --initial-memory=4063232 --max-memory=4063232 \ + +tests-audio-a.wasm: tests-audio-a.o + wasm-ld tests-audio-a.o ../../lib-c/twr.a -o tests-audio-a.wasm \ + --no-entry --shared-memory --no-check-features --initial-memory=4063232 --max-memory=4063232 \ + +clean: + rm -f *.o + rm -f *.wasm \ No newline at end of file diff --git a/examples/tests-audio/clearIODiv.ts b/examples/tests-audio/clearIODiv.ts new file mode 100644 index 00000000..55c85e32 --- /dev/null +++ b/examples/tests-audio/clearIODiv.ts @@ -0,0 +1,29 @@ +import {IWasmModule, IWasmModuleAsync, twrLibrary, keyEventToCodePoint, TLibImports, twrLibraryInstanceRegistry} from "twr-wasm" + +// Libraries use default export +export default class clearIODivLib extends twrLibrary { + id: number; + + imports:TLibImports = { + clearIODiv: {}, + }; + + // every library should have this line + libSourcePath = new URL(import.meta.url).pathname; + + constructor() { + // all library constructors should start with these two lines + super(); + this.id=twrLibraryInstanceRegistry.register(this); + } + + clearIODiv(mod: IWasmModule|IWasmModuleAsync) { + const ioDiv = document.getElementById("twr_iodiv"); + if (!ioDiv) throw new Error("clearIODiv couldn't find twr_iodiv!"); + + ioDiv.innerText = ""; + } + +} + + diff --git a/examples/tests-audio/croud.mp3 b/examples/tests-audio/croud.mp3 new file mode 100644 index 00000000..2e28e047 Binary files /dev/null and b/examples/tests-audio/croud.mp3 differ diff --git a/examples/tests-audio/index.html b/examples/tests-audio/index.html new file mode 100644 index 00000000..ac2bfe32 --- /dev/null +++ b/examples/tests-audio/index.html @@ -0,0 +1,81 @@ + + + + Audio Tests + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/examples/tests-audio/out/clearIODiv.d.ts b/examples/tests-audio/out/clearIODiv.d.ts new file mode 100644 index 00000000..90cf193d --- /dev/null +++ b/examples/tests-audio/out/clearIODiv.d.ts @@ -0,0 +1,9 @@ +import { IWasmModule, IWasmModuleAsync, twrLibrary, TLibImports } from "twr-wasm"; +export default class clearIODivLib extends twrLibrary { + id: number; + imports: TLibImports; + libSourcePath: string; + constructor(); + clearIODiv(mod: IWasmModule | IWasmModuleAsync): void; +} +//# sourceMappingURL=clearIODiv.d.ts.map \ No newline at end of file diff --git a/examples/tests-audio/out/timerLib.d.ts b/examples/tests-audio/out/timerLib.d.ts new file mode 100644 index 00000000..2d077794 --- /dev/null +++ b/examples/tests-audio/out/timerLib.d.ts @@ -0,0 +1,6 @@ +import { IWasmModule, IWasmModuleAsync, twrLibrary, TLibImports } from "twr-wasm"; +export default class timerLib extends twrLibrary { + imports: TLibImports; + setTimeout(mod: IWasmModule | IWasmModuleAsync, eventID: number, time: number, args: number): void; +} +//# sourceMappingURL=timerLib.d.ts.map \ No newline at end of file diff --git a/examples/tests-audio/package.json b/examples/tests-audio/package.json new file mode 100644 index 00000000..3e452583 --- /dev/null +++ b/examples/tests-audio/package.json @@ -0,0 +1,11 @@ +{ + "@parcel/resolver-default": { + "packageExports": true + }, + "alias": { + "twr-wasm": "../../lib-js/index.js" + }, + "dependencies": { + "twr-wasm": "^2.0.0" + } +} diff --git a/examples/tests-audio/ping.mp3 b/examples/tests-audio/ping.mp3 new file mode 100644 index 00000000..5b444ba7 Binary files /dev/null and b/examples/tests-audio/ping.mp3 differ diff --git a/examples/tests-audio/tests-audio.c b/examples/tests-audio/tests-audio.c new file mode 100644 index 00000000..364543fb --- /dev/null +++ b/examples/tests-audio/tests-audio.c @@ -0,0 +1,1005 @@ +#include +#include "twr-crt.h" // twr_register_callback +#include +#include +#include +#include + + +__attribute__((import_name("clearIODiv"))) +void clearIODiv(); + +#define CHANNELS 2 +#define SAMPLE_RATE 48000 +#define SECONDS 3 + +enum Tests { + FromFloatPCMAndGetFloatPCM, + From8bitPCMAndGet8bitPCM, + From16bitPCMAndGet16bitPCM, + From32bitPCMAndGet32bitPCM, + GetMetadata, + PlayAudioFromSample, + PanAudioSample, + PlayLoadedAudio, + AudioFinishCallback, + PlayAudioSync, + SynchronousLoadAudio, + PlayAudioFile, + ModifyPlaybackVolume, + ModifyFilePlaybackVolume, + ModifyPlaybackPan, + ModifyPlaybackRate, + ModifyFilePlaybackRate, + QueryPlaybackSampleAudio, + StopAudioPlaybackSample, + StopAudioPlaybackFile, + StopAudioPlaybackFileLoop, + PlayAudioNodeRange, + PlayAudioNodeRangeLoop, + PlayAudioNodeRangeSampleRate, +}; +const long START_TEST = FromFloatPCMAndGetFloatPCM; +const long END_TEST = PlayAudioNodeRangeSampleRate; + +const char* TEST_NAMES[30] = { + "FromFloatPCMAndGetFloatPCM", + "From8bitPCMAndGet8bitPCM", + "From16bitPCMAndGet16bitPCM", + "From32bitPCMAndGet32bitPCM", + "GetMetadata", + "PlayAudioFromSample", + "PanAudioSample", + "PlayLoadedAudio", + "AudioFinishCallback", + "PlayAudioSync", + "SynchronousLoadAudio", + "PlayAudioFile", + "ModifyPlaybackVolume", + "ModifyFilePlaybackVolume", + "ModifyPlaybackPan", + "ModifyPlaybackRate", + "ModifyFilePlaybackRate", + "QueryPlaybackSampleAudio", + "StopAudioPlaybackSample", + "StopAudioPlaybackFile", + "StopAudioPlaybackFileLoop", + "PlayAudioNodeRange", + "PlayAudioNodeRangeLoop", + "PlayAudioNodeRangeSampleRate", +}; + +float* generate_random_noise(long total_length) { + float* noise = (float*)malloc(sizeof(float) * total_length); + for (int i = 0; i < total_length; i++) { + noise[i] = ((float)(rand() - RAND_MAX/2))/(((float)RAND_MAX)/2.0); + } + + return noise; +} + +void test_fail(const char* test_name, const char* err_msg) { + printf("%s test failed! %s\n", test_name, err_msg); +} +void test_success(const char* test_name) { + printf("%s test was successful!\n", test_name); +} + +enum CallType { + JSCall, + NextTest, + NextTestPart, + AudioLoaded, + PlaybackFinished +}; + +void internal_test_case(int test, void* extra, bool full, enum CallType typ); + + +//used for if user tries to run a test while one is already going +//the requested test is queued up and the running one is canceled ASAP +bool TEST_IS_RUNNING = false; +bool TEST_IN_QUEUE = false; +struct WaitingTest { + int test_id; + bool full; +}; +struct WaitingTest WAITING_TEST; + +void run_queued_test() { + TEST_IN_QUEUE = false; + clearIODiv(); + internal_test_case(WAITING_TEST.test_id, NULL, WAITING_TEST.full, JSCall); +} +//current test_run so it can be interrupted by restarting the tests or running an individual one +int TEST_NEXT_EVENT = -1; +int TEST_NEXT_CURRENT_TEST = -1; +int TEST_NEXT_THERE_IS_NEXT = false; +void test_next(int current_test, bool full, long timeout) { + //test_next is used to interrupt currently running test + //and run the requested one + //this way, assuming no bugs, all other events are cleared and ready for the next test + if (TEST_IN_QUEUE) { + run_queued_test(); + return; + } + int next_test = current_test+1; + // if (full && next_test <= END_TEST) { + if (TEST_NEXT_EVENT == -1) { + TEST_NEXT_EVENT = twr_register_callback("testNextTimer"); + } + + //if the event has a timeout, run it even if there is no text + //just in case a new test is called while it's still running + if (timeout <= 0 && full && next_test <= END_TEST) { + internal_test_case(next_test, 0, true, NextTest); + } else { + TEST_IS_RUNNING = true; + TEST_NEXT_CURRENT_TEST = next_test; + TEST_NEXT_THERE_IS_NEXT = full && next_test <= END_TEST; + twr_timer_single_shot(timeout, TEST_NEXT_EVENT); + } + // } +} +__attribute__((export_name("testNextTimer"))) +void test_next_timer(int event_id) { + if (TEST_IN_QUEUE) { + run_queued_test(); + } else if (TEST_NEXT_THERE_IS_NEXT) { + internal_test_case(TEST_NEXT_CURRENT_TEST, NULL, true, NextTest); + } else { + TEST_IS_RUNNING = false; + } +} + +struct TestDataPart { + int current_test; + bool full; + void* extra; +}; +struct TestDataPart TEST_DATA_PART; +int TEST_NEXT_PART_EVENT = -1; +void test_next_part(int current_test, void* extra, bool full, long timeout) { + TEST_IS_RUNNING = true; + if (TEST_NEXT_PART_EVENT == -1) { + TEST_NEXT_PART_EVENT = twr_register_callback("testNextPartTimer"); + } + + if (timeout <= 0) { + internal_test_case(current_test, extra, full, NextTestPart); + } else { + TEST_DATA_PART.current_test = current_test; + TEST_DATA_PART.extra = extra; + TEST_DATA_PART.full = full; + twr_timer_single_shot(timeout, TEST_NEXT_PART_EVENT); + } +} +__attribute__((export_name("testNextPartTimer"))) +void test_next_part_timer(int event_id) { + internal_test_case(TEST_DATA_PART.current_test, TEST_DATA_PART.extra, TEST_DATA_PART.full, NextTestPart); +} + + +int AUDIO_EVENT_ID = -1; +int AUDIO_LOAD_ID = -1; +int AUDIO_LOAD_CURRENT_TEST = -1; +bool AUDIO_LOAD_FULL = false; +void wait_for_audio_load(int current_test, bool full, char* url) { + assert(AUDIO_LOAD_ID == -1); + + TEST_IS_RUNNING = true; + + if (AUDIO_EVENT_ID == -1) { + AUDIO_EVENT_ID = twr_register_callback("audioLoadEvent"); + } + + long node_id = twr_audio_load(AUDIO_EVENT_ID, url); + + AUDIO_LOAD_ID = node_id; + AUDIO_LOAD_CURRENT_TEST = current_test; + AUDIO_LOAD_FULL = full; +} + +__attribute__((export_name("audioLoadEvent"))) +void audio_load_event(int event_id, int audio_id) { + if (audio_id != AUDIO_LOAD_ID) return; + + int current_test = AUDIO_LOAD_CURRENT_TEST; + int full = AUDIO_LOAD_FULL; + + AUDIO_LOAD_ID = -1; + AUDIO_LOAD_CURRENT_TEST = -1; + AUDIO_LOAD_FULL = -1; + + internal_test_case(current_test, (void*)(audio_id), full, AudioLoaded); +} + +int PLAYBACK_WAIT_EVENT_ID = -1; +int PLAYBACK_WAIT_ID = -1; +int PLAYBACK_WAIT_CURRENT_TEST = -1; +bool PLAYBACK_WAIT_LOAD_FULL = false; +void wait_for_playback_finish(int current_test, bool full, long node_id, long start_sample, long end_sample, int loop, long sample_rate, double volume, double pan) { + assert(PLAYBACK_WAIT_ID == -1); + + TEST_IS_RUNNING = true; + + if (PLAYBACK_WAIT_EVENT_ID == -1) { + PLAYBACK_WAIT_EVENT_ID = twr_register_callback("waitForPlaybackFinishEvent"); + } + + struct PlayRangeFields fields = { + .loop = loop, + .sample_rate = sample_rate, + .volume = volume, + .pan = pan, + .finish_callback = PLAYBACK_WAIT_EVENT_ID + }; + long playback_id = twr_audio_play_range_ex(node_id, start_sample, end_sample, &fields); + + PLAYBACK_WAIT_ID = playback_id; + PLAYBACK_WAIT_CURRENT_TEST = current_test; + PLAYBACK_WAIT_LOAD_FULL = full; +} + +__attribute__((export_name("waitForPlaybackFinishEvent"))) +void wait_for_playback_finish_event(int event_id, int playback_id) { + if (playback_id != PLAYBACK_WAIT_ID) return; + + int current_test = PLAYBACK_WAIT_CURRENT_TEST; + int full = PLAYBACK_WAIT_LOAD_FULL; + + PLAYBACK_WAIT_ID = -1; + PLAYBACK_WAIT_CURRENT_TEST = -1; + PLAYBACK_WAIT_LOAD_FULL = false; + + internal_test_case(current_test, (void*)playback_id, full, PlaybackFinished); +} + +#define ERR_MSG_LEN 30 +char err_msg[ERR_MSG_LEN]; + +void test_from_pcm_and_get_pcm( + long test, + long channels, + long duration, + long sample_rate, + void* data, + long (*generate_audio)(long, long, void*, long), + void* (*get_audio)(long, long*, long*), + bool (*validate)(void* data1, void* data2, long len) +) { + long node_id = generate_audio(channels, sample_rate, data, sample_rate*duration); + + long total_len = duration*sample_rate*channels; + + long num_channels = -1; + + long single_channel_len = -1; + + void* test_noise = twr_audio_get_float_pcm(node_id, &single_channel_len, &num_channels); + + + if (num_channels != CHANNELS) { + snprintf(err_msg, ERR_MSG_LEN-1, "expected %d channels, got %ld", CHANNELS, num_channels); + test_fail(TEST_NAMES[test], err_msg); + } else if (single_channel_len*num_channels != total_len) { + snprintf(err_msg, ERR_MSG_LEN-1, "expected a length of %ld, got %ld", total_len, single_channel_len*num_channels); + test_fail(TEST_NAMES[test], err_msg); + } else if (validate(data, test_noise, total_len)) { + test_success(TEST_NAMES[test]); + } else { + test_fail(TEST_NAMES[test], "Given audio didn't match what was retrieved!"); + } + + twr_audio_free_id(node_id); + + free(test_noise); +} + +bool validate_float(void* data1, void* data2, long len) { + for (long i = 0; i < len; i++) { + if (((float*)data1)[i] != ((float*)data2)[i]) { + return false; + } + } + return true; +} +bool validate_char(void* data1, void* data2, long len) { + for (long i = 0; i < len; i++) { + if (((char*)data1)[i] != ((char*)data2)[i]) { + return false; + } + } + return true; +} +bool validate_short(void* data1, void* data2, long len) { + for (long i = 0; i < len; i++) { + if (((short*)data1)[i] != ((short*)data2)[i]) { + return false; + } + } + return true; +} +bool validate_int(void* data1, void* data2, long len) { + for (long i = 0; i < len; i++) { + if (((int*)data1)[i] != ((int*)data2)[i]) { + return false; + } + } + return true; +} +void internal_test_case(int test, void* extra, bool full, enum CallType typ) { + //set TEST_IS_RUNNING to false. A callback like wait_for_audio_load, test_next, or test_next_part + // will set it back to true if needed. Otherwise, the test will have finished + TEST_IS_RUNNING = false; + + switch (test) { + case FromFloatPCMAndGetFloatPCM: + { + float* noise = generate_random_noise(CHANNELS * SAMPLE_RATE * SECONDS); + test_from_pcm_and_get_pcm(test, CHANNELS, SECONDS, SAMPLE_RATE, (void*)noise, + (long (*)(long, long, void*, long))twr_audio_from_float_pcm, + (void* (*)(long, long*, long*))twr_audio_get_float_pcm, + validate_float + ); + free(noise); + test_next(test, full, 0); + } + break; + case From8bitPCMAndGet8bitPCM: + { + long total_len = CHANNELS * SAMPLE_RATE * SECONDS; + float* noise = generate_random_noise(total_len); + char* noise_char = (char*)malloc(sizeof(char) * total_len); + for (long i = 0; i < total_len; i++) { + noise_char[i] = (char)floor(noise[i]/128 + 0.5); //0.5 is added to make it round + } + free(noise); + + test_from_pcm_and_get_pcm(test, CHANNELS, SECONDS, SAMPLE_RATE, (void*)noise_char, + (long (*)(long, long, void*, long))twr_audio_from_8bit_pcm, + (void* (*)(long, long*, long*))twr_audio_get_8bit_pcm, + validate_char + ); + free(noise_char); + test_next(test, full, 0); + } + break; + case From16bitPCMAndGet16bitPCM: + { + long total_len = CHANNELS * SAMPLE_RATE * SECONDS; + float* noise = generate_random_noise(total_len); + short* noise_char = (short*)malloc(sizeof(short) * total_len); + for (long i = 0; i < total_len; i++) { + noise_char[i] = (short)floor(noise[i]/32768 + 0.5); //0.5 is added to make it round + } + free(noise); + + test_from_pcm_and_get_pcm(test, CHANNELS, SECONDS, SAMPLE_RATE, (void*)noise_char, + (long (*)(long, long, void*, long))twr_audio_from_16bit_pcm, + (void* (*)(long, long*, long*))twr_audio_get_16bit_pcm, + validate_short + ); + free(noise_char); + test_next(test, full, 0); + } + break; + case From32bitPCMAndGet32bitPCM: + { + long total_len = CHANNELS * SAMPLE_RATE * SECONDS; + float* noise = generate_random_noise(total_len); + int* noise_char = (int*)malloc(sizeof(int) * total_len); + for (long i = 0; i < total_len; i++) { + noise_char[i] = (int)floor(noise[i]/2147483648 + 0.5); //0.5 is added to make it round + } + free(noise); + + test_from_pcm_and_get_pcm(test, CHANNELS, SECONDS, SAMPLE_RATE, (void*)noise_char, + (long (*)(long, long, void*, long))twr_audio_from_32bit_pcm, + (void* (*)(long, long*, long*))twr_audio_get_32bit_pcm, + validate_int + ); + free(noise_char); + test_next(test, full, 0); + } + break; + + case GetMetadata: + { + float* noise = generate_random_noise(CHANNELS * SAMPLE_RATE * SECONDS); + long node_id = twr_audio_from_float_pcm(CHANNELS, SAMPLE_RATE, noise, SAMPLE_RATE * SECONDS); + free(noise); + + struct AudioMetadata meta; + twr_audio_get_metadata(node_id, &meta); + + if (meta.channels != CHANNELS || meta.length != SAMPLE_RATE * SECONDS || meta.sample_rate != SAMPLE_RATE) { + test_fail(TEST_NAMES[test], "metadata didn't match given values"); + } else { + + twr_audio_get_metadata(node_id, &meta); + + if (meta.channels == CHANNELS && meta.length == SAMPLE_RATE * SECONDS && meta.sample_rate == SAMPLE_RATE) { + test_success(TEST_NAMES[test]); + } else { + char err_msg[100]; + snprintf( + err_msg, + 99, + "appended metadata expected: %ld == %d, %ld == %d, %ld == %d", + meta.channels, CHANNELS, + meta.sample_rate, SAMPLE_RATE, + meta.length, SAMPLE_RATE * SECONDS + ); + test_fail(TEST_NAMES[test], err_msg); + } + free(extra); + } + twr_audio_free_id(node_id); + + test_next(test, full, 0); + } + break; + + case PlayAudioFromSample: + { + float* noise = generate_random_noise(CHANNELS * SAMPLE_RATE * SECONDS); + long node_id = twr_audio_from_float_pcm(CHANNELS, SAMPLE_RATE, noise, SAMPLE_RATE*SECONDS); + + twr_audio_play_volume(node_id, 0.5, 0.0); + printf("Running test %s\n", TEST_NAMES[test]); + + twr_audio_free_id(node_id); + free(noise); + test_next(test, full, SECONDS*1000 + 500); + } + break; + + case PanAudioSample: + { + float* noise = generate_random_noise(CHANNELS * SAMPLE_RATE * SECONDS); + long node_id = twr_audio_from_float_pcm(CHANNELS, SAMPLE_RATE, noise, SAMPLE_RATE*SECONDS); + + twr_audio_play_volume(node_id, 0.5, -0.5); + printf("Running test %s\n", TEST_NAMES[test]); + + twr_audio_free_id(node_id); + free(noise); + test_next(test, full, SECONDS*1000 + 500); + } + break; + + case PlayLoadedAudio: + { + #ifdef ASYNC + long node_id = twr_audio_load_sync("ping.mp3"); + twr_audio_play(node_id); + printf("Running test %s\n", TEST_NAMES[test]); + twr_audio_free_id(node_id); + test_next(test, full, 3000); + #else + printf("%s can only be ran as async!\n", TEST_NAMES[test]); + test_next(test, full, 0); + #endif + } + break; + + case AudioFinishCallback: + { + if (typ != PlaybackFinished) { + float* noise = generate_random_noise(CHANNELS * SAMPLE_RATE * SECONDS); + long node_id = twr_audio_from_float_pcm(CHANNELS, SAMPLE_RATE, noise, SAMPLE_RATE*SECONDS); + + printf("Running test %s\n", TEST_NAMES[test]); + wait_for_playback_finish(test, full, node_id, 0, SAMPLE_RATE*SECONDS, false, SAMPLE_RATE, 50, 0); + + twr_audio_free_id(node_id); + free(noise); + + } else { + long playback_id = (long)extra; + if (twr_audio_query_playback_position(playback_id) == -1) { + test_success(TEST_NAMES[test]); + } else { + test_fail(TEST_NAMES[test], "audio playback node hasn't been cleared yet!"); + } + test_next(test, full, 0); + } + } + break; + + case PlayAudioSync: + { + #ifdef ASYNC + float* noise = generate_random_noise(CHANNELS * SAMPLE_RATE * SECONDS); + long node_id = twr_audio_from_float_pcm(CHANNELS, SAMPLE_RATE, noise, SAMPLE_RATE*SECONDS); + + printf("Running test %s\n", TEST_NAMES[test]); + + long playback_id = twr_audio_play_sync(node_id); + + twr_audio_free_id(node_id); + free(noise); + + if (twr_audio_query_playback_position(playback_id) == -1) { + test_success(TEST_NAMES[test]); + } else { + test_fail(TEST_NAMES[test], "audio playback node hasn't been cleared yet!"); + } + #else + printf("%s can only be ran as async!\n", TEST_NAMES[test]); + #endif + test_next(test, full, 0); + } + break; + + case SynchronousLoadAudio: + { + if (typ != AudioLoaded) { + wait_for_audio_load(test, full, "ping.mp3"); + } else { + long node_id = (long)extra; + twr_audio_play(node_id); + + printf("Running test %s\n", TEST_NAMES[test]); + + twr_audio_free_id(node_id); + test_next(test, full, 3000); + } + + } + break; + + case PlayAudioFile: + { + twr_audio_play_file("ping.mp3"); + printf("Running test %s\n", TEST_NAMES[test]); + test_next(test, full, 3000); + } + break; + + case ModifyPlaybackVolume: + { + if (extra == NULL) { + float* noise = generate_random_noise(CHANNELS * SAMPLE_RATE * SECONDS); + long node_id = twr_audio_from_float_pcm(CHANNELS, SAMPLE_RATE, noise, SAMPLE_RATE*SECONDS); + + printf("Running test %s\n", TEST_NAMES[test]); + + long playback_id = twr_audio_play(node_id); + + twr_audio_free_id(node_id); + free(noise); + + void* args = malloc(sizeof(double) + sizeof(long)); + *(double*)(args) = 1.0; + *(long*)(args + sizeof(double)) = playback_id; + + test_next_part(test, (void*)args, full, (SECONDS * 1000)/10); + } else { + double* vol_ptr = (double*)extra; + long playback_id = *(long*)(extra + sizeof(double)); + *vol_ptr -= 0.1; + double volume = *vol_ptr; + + if (twr_audio_query_playback_position(playback_id) == -1) { + free(extra); + test_next(test, full, 0); + } else { + twr_audio_modify_playback_volume(playback_id, volume); + test_next_part(test, extra, full, (SECONDS * 1000)/10); + } + } + } + break; + + case ModifyFilePlaybackVolume: + { + if (extra == NULL) { + + printf("Running test %s\n", TEST_NAMES[test]); + + long playback_id = twr_audio_play_file("croud.mp3"); + + void* args = malloc(sizeof(double) + sizeof(long)); + *(double*)(args) = 1.0; + *(long*)(args + sizeof(double)) = playback_id; + + test_next_part(test, (void*)args, full, (4 * 1000)/10); + } else { + double* vol_ptr = (double*)extra; + long playback_id = *(long*)(extra + sizeof(double)); + *vol_ptr -= 0.1; + double volume = *vol_ptr; + + if (twr_audio_query_playback_position(playback_id) == -1) { + free(extra); + test_next(test, full, 0); + } else { + twr_audio_modify_playback_volume(playback_id, volume); + test_next_part(test, extra, full, (4 * 1000)/10); + } + } + } + break; + + case ModifyPlaybackPan: + { + if (extra == NULL) { + float* noise = generate_random_noise(CHANNELS * SAMPLE_RATE * SECONDS); + long node_id = twr_audio_from_float_pcm(CHANNELS, SAMPLE_RATE, noise, SAMPLE_RATE*SECONDS); + + printf("Running test %s\n", TEST_NAMES[test]); + + long playback_id = twr_audio_play_volume(node_id, 0.5, -1.0); + + twr_audio_free_id(node_id); + free(noise); + + void* args = malloc(sizeof(double) + sizeof(long)); + *(double*)(args) = -1.0; + *(long*)(args + sizeof(double)) = playback_id; + + test_next_part(test, (void*)args, full, (SECONDS * 1000)/26); + } else { + double* pan_ptr = (double*)extra; + long playback_id = *(long*)(extra + sizeof(double)); + *pan_ptr += 1.0/20.0*2.0; + double pan = *pan_ptr; + pan = pan > 1.0 ? 1.0 : pan; + // pan = pan > 1.0 ? 1.0 : pan; + + if (twr_audio_query_playback_position(playback_id) == -1) { + free(extra); + test_next(test, full, 0); + } else { + twr_audio_modify_playback_pan(playback_id, pan); + test_next_part(test, extra, full, (SECONDS * 1000)/26); + } + } + } + break; + + case ModifyPlaybackRate: + { + + if (typ != AudioLoaded && typ != NextTestPart) { + wait_for_audio_load(test, full, "ping.mp3"); + } else if (typ == AudioLoaded) { + printf("Running test %s\n", TEST_NAMES[test]); + + long node_id = (long)extra; + + struct AudioMetadata meta; + twr_audio_get_metadata(node_id, &meta); + + long START_SAMPLE_RATE = meta.sample_rate; + + struct PlayRangeFields fields = twr_audio_default_play_range(); + fields.sample_rate = START_SAMPLE_RATE; + fields.volume = 0.5; + + long playback_id = twr_audio_play_range_ex(node_id, 0, meta.length, &fields); + + twr_audio_free_id(node_id); + + long* args = malloc(sizeof(long) * 2); + args[0] = playback_id; + + args[1] = meta.sample_rate; + + test_next_part(test, (void*)args, full, (SECONDS * 1000)/20); + + } else { + long* args = (long*)extra; + long playback_id = args[0]; + args[1] = (long)((args[1])/1.15); + long rate = args[1]; + + if (twr_audio_query_playback_position(playback_id) == -1) { + free(args); + test_next(test, full, 0); + } else { + twr_audio_modify_playback_rate(playback_id, rate); + test_next_part(test, (void*)args, full, (SECONDS * 1000)/20); + } + } + } + break; + + case ModifyFilePlaybackRate: + { + if (extra == NULL) { + printf("Running test %s\n", TEST_NAMES[test]); + + long playback_id = twr_audio_play_file("croud.mp3"); + + void* args = malloc(sizeof(double) + sizeof(long)); + + *(double*)args = 1.0; + *(long*)(args + sizeof(double)) = playback_id; + + test_next_part(test, (void*)args, full, (4 * 1000)/5); + + } else { + double* rate_ptr = (double*)extra; + long playback_id = *(long*)(extra + sizeof(double)); + + *rate_ptr *= 2.0; + long rate = *rate_ptr; + + if (twr_audio_query_playback_position(playback_id) == -1) { + free(extra); + test_next(test, full, 0); + } else { + twr_audio_modify_playback_rate(playback_id, rate); + test_next_part(test, extra, full, (SECONDS * 1000)/5); + } + } + } + break; + + case QueryPlaybackSampleAudio: + { + long* state = (long*)extra; + if (typ != NextTestPart) { + state = malloc(sizeof(long) * 3); + float* noise = generate_random_noise(CHANNELS * SAMPLE_RATE * SECONDS); + long node_id = twr_audio_from_float_pcm(CHANNELS, SAMPLE_RATE, noise, SAMPLE_RATE*SECONDS); + + long playback_id = twr_audio_play_volume(node_id, 0.5, 0.0); + twr_audio_free_id(node_id); + free(noise); + state[0] = playback_id; + state[1] = twr_epoch_timems(); + state[2] = true; + } + + long elapsed = twr_epoch_timems() - state[1]; + + if (elapsed <= SECONDS*1000) { + // printf("playback position: %ld, elapsed: %ld\n", twr_audio_query_playback_position(state[0]), elapsed); + long pos = twr_audio_query_playback_position(state[0]); + if (abs(elapsed - pos) > 20) { + state[2] = false; + } + test_next_part(test, (void*)state, full, 200); + } else { + if (state[2]) { + test_success(TEST_NAMES[test]); + } else { + test_fail(TEST_NAMES[test], "audio played at an unexpected rate!"); + } + free(state); + test_next(test, full, 0); + } + } + break; + + case StopAudioPlaybackSample: + { + long prev_playback_id = (long)extra; + if (typ != NextTestPart) { + float* noise = generate_random_noise(CHANNELS * SAMPLE_RATE * SECONDS); + long node_id = twr_audio_from_float_pcm(CHANNELS, SAMPLE_RATE, noise, SAMPLE_RATE*SECONDS); + free(noise); + long playback_id = twr_audio_play(node_id); + twr_audio_free_id(node_id); + + twr_audio_stop_playback(playback_id); + + //playback is fully deleted based on an event, so it might take time to propogate + test_next_part(test, (void*)playback_id, full, 200); + } else { + if (twr_audio_query_playback_position(prev_playback_id) < 0) { + test_success(TEST_NAMES[test]); + } else { + test_fail(TEST_NAMES[test], "audio hasn't ended!"); + } + test_next(test, full, 0); + } + } + break; + + case StopAudioPlaybackFile: + { + long* ret_args = (long*)extra; + long prev_playback_id = ret_args[0]; + if (typ != NextTestPart) { + long playback_id = twr_audio_play_file("croud.mp3"); + + long* args = malloc(sizeof(long) * 2); + args[0] = -20; + args[1] = playback_id; + + //audio takes a bit to load, give it some time before stopping + test_next_part(test, (void*)args, full, 200); + } else if (prev_playback_id == -20) { + long playback_id = ret_args[1]; + free(extra); + + twr_audio_stop_playback(playback_id); + + //give it some time to propogate the stop + test_next_part(test, (void*)playback_id, full, 200); + } else { + if (twr_audio_query_playback_position(prev_playback_id) < 0) { + test_success(TEST_NAMES[test]); + } else { + test_fail(TEST_NAMES[test], "audio hasn't ended!"); + } + test_next(test, full, 0); + } + } + break; + + case StopAudioPlaybackFileLoop: + { + long* ret_args = (long*)extra; + long prev_playback_id = ret_args[0]; + if (typ != NextTestPart) { + long playback_id = twr_audio_play_file_ex("croud.mp3", 1.0, 0.0, true); + + long* args = malloc(sizeof(long) * 2); + args[0] = -20; + args[1] = playback_id; + + //audio takes a bit to load, give it some time before stopping + test_next_part(test, (void*)args, full, 200); + } else if (prev_playback_id == -20) { + long playback_id = ret_args[1]; + free(extra); + + twr_audio_stop_playback(playback_id); + + //give it some time to propogate the stop + test_next_part(test, (void*)playback_id, full, 200); + } else { + if (twr_audio_query_playback_position(prev_playback_id) < 0) { + test_success(TEST_NAMES[test]); + } else { + test_fail(TEST_NAMES[test], "audio hasn't ended!"); + } + test_next(test, full, 0); + } + } + break; + + case PlayAudioNodeRange: + { + long prev_id = (long)extra; + long target_runtime = 1; + if (typ != NextTestPart) { + float* noise = generate_random_noise(SAMPLE_RATE * SECONDS * CHANNELS); + long node_id = twr_audio_from_float_pcm(CHANNELS, SAMPLE_RATE, noise, SAMPLE_RATE * SECONDS); + free(noise); + + long playback_id = twr_audio_play_range(node_id, 0, SAMPLE_RATE * target_runtime); + + twr_audio_free_id(node_id); + + test_next_part(test, (void*)playback_id, full, (long)(((double)target_runtime) * 1000.0 * 1.25)); + } else { + long runtime = twr_audio_query_playback_position(prev_id); + //timeout is longer than the audio sample range given + //the audio should be done (returning -1) by the time it gets here + if (runtime == -1) { + test_success(TEST_NAMES[test]); + test_next(test, full, 0); + } else { + test_fail(TEST_NAMES[test], "Audio played for too long!"); + //have a different timeout to ensure this one finished before next possible test + test_next(test, full, (SECONDS - target_runtime) * 1000); + } + } + } + break; + + case PlayAudioNodeRangeLoop: + { + long prev_id = (long)extra; + double target_runtime = 0.15; + if (typ != NextTestPart) { + float* noise = generate_random_noise(SAMPLE_RATE * SECONDS * CHANNELS); + long node_id = twr_audio_from_float_pcm(CHANNELS, SAMPLE_RATE, noise, SAMPLE_RATE * SECONDS); + free(noise); + + struct PlayRangeFields fields = twr_audio_default_play_range(); + fields.loop = true; + + long playback_id = twr_audio_play_range_ex(node_id, 0, SAMPLE_RATE * target_runtime, &fields); + + twr_audio_free_id(node_id); + + test_next_part(test, (void*)playback_id, full, (long)(target_runtime * 1000.0 * 10.0)); + } else { + long runtime = twr_audio_query_playback_position(prev_id); + //timeout is longer than the audio sample range given + //since the audio loops, it should not be done by the time it get's here + if (runtime != -1) { + test_success(TEST_NAMES[test]); + twr_audio_stop_playback(prev_id); + } else { + test_fail(TEST_NAMES[test], "audio failed to loop!"); + } + test_next(test, full, 0); + } + } + break; + + case PlayAudioNodeRangeSampleRate: + { + long prev_id = (long)extra; + double target_runtime = 1.0; + long n_sample_rate = SAMPLE_RATE * 2; + if (typ != NextTestPart) { + float* noise = generate_random_noise(SAMPLE_RATE * SECONDS * CHANNELS); + long node_id = twr_audio_from_float_pcm(CHANNELS, SAMPLE_RATE, noise, SAMPLE_RATE * SECONDS); + free(noise); + + struct PlayRangeFields fields = twr_audio_default_play_range(); + fields.sample_rate = n_sample_rate; + + long playback_id = twr_audio_play_range_ex(node_id, 0, SAMPLE_RATE * target_runtime, &fields); + + twr_audio_free_id(node_id); + + long timeout = (long)(target_runtime * 1000.0 * ((n_sample_rate + SAMPLE_RATE)/2.0)/n_sample_rate); + test_next_part(test, (void*)playback_id, full, timeout); + + } else { + long runtime = twr_audio_query_playback_position(prev_id); + //timeout is longer than the audio sample range given + //since the audio loops, it should not be done by the time it get's here + if (runtime == -1) { + test_success(TEST_NAMES[test]); + } else { + test_fail(TEST_NAMES[test], "audio lasted longer than expected"); + twr_audio_stop_playback(prev_id); + } + test_next(test, full, 0); + } + } + break; + + + + default: + assert(false); + } + + // printf("test running: %d\n", TEST_IS_RUNNING); +} + +void enter_test_case(int test, bool full) { + if (!TEST_IS_RUNNING) { + clearIODiv(); + internal_test_case(test + START_TEST, 0, full, JSCall); + } else { + TEST_IN_QUEUE = true; + WAITING_TEST.test_id = test; + WAITING_TEST.full = full; + } +} + +__attribute__((export_name("testCase"))) +void test_case(int test) { + // internal_test_case(test + START_TEST, 0, false, JSCall); + enter_test_case(test, false); +} + +__attribute__((export_name("testAll"))) +void test_all() { + // internal_test_case(START_TEST, 0, true, JSCall); + enter_test_case(0, true); +} + +__attribute__((export_name("getNumTests"))) +int get_num_tests() { + return END_TEST - START_TEST + 1; +} + +__attribute__((export_name("getTestName"))) +const char* get_test_name(int id) { + return TEST_NAMES[id+START_TEST]; +} \ No newline at end of file diff --git a/examples/tests-audio/tsconfig.json b/examples/tests-audio/tsconfig.json new file mode 100644 index 00000000..c8beff3b --- /dev/null +++ b/examples/tests-audio/tsconfig.json @@ -0,0 +1,36 @@ + { + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + "incremental": false, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + "composite": false, /* Enable constraints that allow a TypeScript project to be used with project references. */ + + /* Language and Environment */ + "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": ["ES2023","DOM"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + + /* Modules */ + "module": "ESNext", /* Specify what module code is generated. */ + "moduleResolution": "Bundler", + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "declarationMap": true, /* Create sourcemaps for d.ts files. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + "outDir": "./out", /* Specify an output folder for all emitted files. */ + + /* Interop Constraints */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "paths": { + "twr-wasm": ["./../../lib-js/index"] + } + }, + "files": ["clearIODiv.ts"] + } + + + \ No newline at end of file diff --git a/examples/tests-d2d/Makefile b/examples/tests-d2d/Makefile new file mode 100644 index 00000000..ef600103 --- /dev/null +++ b/examples/tests-d2d/Makefile @@ -0,0 +1,31 @@ +CC := clang +TWRCFLAGS := --target=wasm32 -nostdinc -nostdlib -isystem ../../include +CPPLIB := ../twr-cpp +CFLAGS := -c -Wall -O3 $(TWRCFLAGS) -I $(CPPLIB) +CFLAGS_DEBUG := -c -Wall -g -O0 $(TWRCFLAGS) -I $(CPPLIB) + + + +.PHONY: default + +default: tests-d2d.wasm tests-d2d-a.wasm + +tests-d2d.o: tests-d2d.c + $(CC) $(CFLAGS) $< -o $@ + +tests-d2d-a.o: tests-d2d.c + $(CC) $(CFLAGS) $< -o $@ -DASYNC + +tests-d2d.wasm: tests-d2d.o + wasm-ld tests-d2d.o ../../lib-c/twr.a -o tests-d2d.wasm \ + --no-entry --initial-memory=4063232 --max-memory=4063232 \ + --export=test_all --export=test_specific --export get_num_tests --export get_test_name + +tests-d2d-a.wasm: tests-d2d-a.o + wasm-ld tests-d2d-a.o ../../lib-c/twr.a -o tests-d2d-a.wasm \ + --no-entry --shared-memory --no-check-features --initial-memory=4063232 --max-memory=4063232 \ + --export=test_all --export=test_specific --export get_num_tests --export get_test_name + +clean: + rm -f *.o + rm -f *.wasm \ No newline at end of file diff --git a/examples/tests-d2d/index.html b/examples/tests-d2d/index.html new file mode 100644 index 00000000..d9007ec8 --- /dev/null +++ b/examples/tests-d2d/index.html @@ -0,0 +1,60 @@ + + + + Tests-D2D + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/examples/tests-d2d/package.json b/examples/tests-d2d/package.json new file mode 100644 index 00000000..60b49279 --- /dev/null +++ b/examples/tests-d2d/package.json @@ -0,0 +1,11 @@ +{ + "@parcel/resolver-default": { + "packageExports": true + }, + "alias": { + "twr-wasm": "../../lib-js/index.js" + }, + "dependencies": { + "twr-wasm": "^2.0.0" + } +} diff --git a/examples/tests-d2d/test-img.jpg b/examples/tests-d2d/test-img.jpg new file mode 100644 index 00000000..d092b9f2 Binary files /dev/null and b/examples/tests-d2d/test-img.jpg differ diff --git a/examples/tests-d2d/tests-d2d.c b/examples/tests-d2d/tests-d2d.c new file mode 100644 index 00000000..ae598e60 --- /dev/null +++ b/examples/tests-d2d/tests-d2d.c @@ -0,0 +1,817 @@ +#include +#include "twr-draw2d.h" +#include +#include +#include +#include +#include + +unsigned long crc32(char* str, unsigned long len) { + const long table_len = 256; + unsigned long crc_table[table_len]; + unsigned long c; + for (unsigned long n = 0; n < table_len; n++) { + c = n; + for (unsigned long k = 0; k < 8; k++) { + c = (c & 1) ? (0xEDB88320 ^ (c >> 1)) : (c >> 1); + } + crc_table[n] = c; + } + + unsigned long crc = (unsigned long)((long)0 ^ (-1)); + + for (char* str_pos = str; str_pos < str+len; str_pos++) { + crc = (crc >> 8) ^ crc_table[(crc ^ *str_pos) & 0xFF]; + } + + return (unsigned long)((long)crc ^ (-1)) >> 0; +} + +unsigned short fletcher16(char* str) { + unsigned short sum1 = 0xFF; + unsigned short sum2 = 0xFF; + + for (char* str_ptr = str; *str_ptr != '\0'; str_ptr++) { + sum1 = (sum1 + *str_ptr) % 255; + sum2 = (sum2 + sum1) % 255; + } + + return (sum2 << 8) | sum1; +} + +void to_hex_string(unsigned long num, char* hex) { + for (unsigned long i = 0; i < 8; i++) { + int seg = (int)((num >> i*4) & 0xF); + hex[7-i] = (char)(seg <= 9 ? '0'+seg : 'A'+seg-10); + } + hex[8] = '\0'; +} + + +void test_hashes(bool print_result, const char* test, const unsigned long* expected_hashes, long expected_hashes_len, unsigned long calc_hash) { + if (!print_result) { + return; + } + + for (long i = 0; i < expected_hashes_len; i++) { + if (expected_hashes[i] == calc_hash) { + printf("%s test was successful!\n", test); + return; + } + } + + printf("%s test failed! Expected ", test); + for (long i = 0; i < expected_hashes_len; i++) { + char hex[9]; + to_hex_string(expected_hashes[i], hex); + if (i != 0) { + printf(", "); + if (i == expected_hashes_len-1) { + printf("or "); + } + } + printf("0x%s", hex); + } + char calc_hex[9]; + to_hex_string(calc_hash, calc_hex); + printf(" got 0x%s\n", calc_hex); +} +void test_hash(bool print_result, const char* test, unsigned long expected_hash, const unsigned long calc_hash) { + test_hashes(print_result, test, &expected_hash, 1, calc_hash); +} +#define width 600 +#define height 600 +#define mem_size width*height*4 +char mem[mem_size]; +void test_img_hashes(struct d2d_draw_seq* ds, bool print_result, const char* test, const unsigned long* hashes, long hashes_len) { + if (!print_result) { + return; + } + + d2d_getimagedata(ds, 1, 0, 0, width, height); + d2d_imagedatatoc(ds, 1, (void*)mem, mem_size); + d2d_releaseid(ds, 1); + unsigned long canvas_hash = crc32(mem, mem_size); + + test_hashes(print_result, test, hashes, hashes_len, canvas_hash); +} +void test_img_hash(struct d2d_draw_seq* ds, bool print_result, const char* test, const unsigned long hash) { + test_img_hashes(ds, print_result, test, &hash, 1); +} + +enum Test { + EmptyCanvas, + FillRect, + Reset, + StrokeRect, + FillText, + FillCodePoint, + StrokeText, + + TextMetrics, + SetLineWidth, + SaveAndRestore, + + SetStrokeStyleRGBA, + SetFillStyleRGBA, + SetStrokeStyle, + SetFillStyle, + SetFont, + SetLineCap, + SetLineJoin, + SetLineDash, + GetLineDashLength, + GetLineDash, + SetLineDashOffset, + + BeginPathAndRectAndFill, + RectAndStroke, + MoveToAndLineTo, + Arc, + ArcTo, + BezierTo, + RoundRect, + Ellipse, + QuardraticCurveTo, + ClosePath, + + ClearRect, + Scale, + Translate, + Rotate, + GetTransform, + SetTransform, + Transform, + ResetTransform, + + CreateLinearGradient, //also tests releaseid, setfillstylegradient, and addColorStop + CreateRadialGradient, + + CToImageDataAndPutImageData, + LoadAndDrawImage, + LoadAndDrawCroppedImage, + + GetCanvasPropDouble, + GetCanvasPropString, + SetCanvasPropDouble, + SetCanvasPropString, +}; + +const int START_TEST = EmptyCanvas; +const int END_TEST = SetCanvasPropString; + +const char* test_strs[50] = { + "EmptyCanvas", + "FillRect", + "Reset", + "StrokeRect", + "FillText", + "FillCodePoint", + "StrokeText", + + "TextMetrics", + "SetLineWidth", + "SaveAndRestore", + + "SetStrokeStyleRGBA", + "SetFillStyleRGBA", + "SetStrokeStyle", + "SetFillStyle", + "SetFont", + "SetLineCap", + "SetLineJoin", + "SetLineDash", + "GetLineDashLength", + "GetLineDash", + "SetLineDashOffset", + + "BeginPathAndRectAndFill", + "RectAndStroke", + "MoveToAndLineTo", + "Arc", + "ArcTo", + "BezierTo", + "RoundRect", + "Ellipse", + "QuardraticCurveTo", + "ClosePath", + + "ClearRect", + "Scale", + "Translate", + "Rotate", + "GetTransform", + "SetTransform", + "Transform", + "ResetTransform", + + "CreateLinearGradient", + "CreateRadialGradient", + + "CToImageDataAndPutImageData", + "LoadAndDrawImage", + "LoadAndDrawCroppedImage", + + "GetCanvasPropDouble", + "GetCanvasPropString", + "SetCanvasPropDouble", + "SetCanvasPropString", +}; + +void test_case(int id, bool first_run) { + struct d2d_draw_seq* ds = d2d_start_draw_sequence(1000); + d2d_reset(ds); + + switch (id) { + case EmptyCanvas: + { + test_img_hash(ds, first_run, test_strs[id], 0xEBF5A8C4); + } + break; + + case FillRect: + { + d2d_fillrect(ds, 10.0, 10.0, 300.0, 300.0); + test_img_hash(ds, first_run, test_strs[id], 0xDF03FF9D); + } + break; + + case Reset: + { + //just uses the reset called above + test_img_hash(ds, first_run, test_strs[id], 0xEBF5A8C4); + } + break; + + case StrokeRect: + { + d2d_strokerect(ds, 10.0, 10.0, 300.0, 300.0); + test_img_hash(ds, first_run, test_strs[id], 0xECBD3A0D); + } + break; + + case FillText: + { + d2d_filltext(ds, "Test Text", 50.0, 50.0); + #define FILLTEXT_HASH_LEN 3 + const unsigned long hashes[FILLTEXT_HASH_LEN] = { + 0xFF60789D, //JohnDog3112 - Laptop + 0xAB2D27E9, //JohnDog3112 - Desktop + 0x9EE6A915 //twiddlingbits + }; + test_img_hashes(ds, first_run, test_strs[id], hashes, FILLTEXT_HASH_LEN); + } + break; + + case FillCodePoint: + { + d2d_fillcodepoint(ds, 65, 50.0, 50.0); + #define FILLCODEPOINT_HASH_LEN 3 + const unsigned long hashes[FILLCODEPOINT_HASH_LEN] = { + 0x1AC59B92, //JohnDog3112 - Laptop + 0xE0736ADD, //JohnDog3112 - Desktop + 0xCC99A006, //twiddlingbits + + }; + test_img_hashes(ds, first_run, test_strs[id], hashes, FILLCODEPOINT_HASH_LEN); + } + break; + + case StrokeText: + { + d2d_stroketext(ds, "Test Text", 50.0, 50.0); + #define STROKETEXT_HASH_LEN 3 + const unsigned long hashes[STROKETEXT_HASH_LEN] = { + 0x6ECCA58D, //JohnDog3112 - Laptop + 0xCB11F126, //JohnDog3112 - Desktop + 0x1BAE7F1C, //twiddlingbits + }; + test_img_hashes(ds, first_run, test_strs[id], hashes, STROKETEXT_HASH_LEN); + } + break; + + case TextMetrics: + { + struct d2d_text_metrics metrics; + d2d_measuretext(ds, "Test Text", &metrics); + #define TEXTMETRICS_HASH_LEN 3 + const unsigned long hashes[TEXTMETRICS_HASH_LEN] = { + 0x8FA86413, //JohnDog3112 - Laptop, + 0xBB895670, //JohnDog3112 - Desktop, + 0xE85EA0D7, //twiddlingbits + }; + test_hashes(first_run, "TextMetrics", hashes, TEXTMETRICS_HASH_LEN, crc32((char*)&metrics, sizeof(struct d2d_text_metrics))); + } + break; + + case SetLineWidth: + { + d2d_setlinewidth(ds, 10.0); + d2d_strokerect(ds, 50.0, 50.0, 500.0, 500.0); + test_img_hash(ds, first_run, test_strs[id], 0x2609FF7F); + } + break; + + case SaveAndRestore: + { + d2d_setlinewidth(ds, 50.0); + d2d_save(ds); + d2d_setlinewidth(ds, 10.0); + d2d_restore(ds); + d2d_strokerect(ds, 50.0, 50.0, 500.0, 500.0); + test_img_hash(ds, first_run, test_strs[id], 0x256026AB); + } + break; + + case SetStrokeStyleRGBA: + { + d2d_setstrokestylergba(ds, 0xFF00FF30); + d2d_strokerect(ds, 50.0, 50.0, 500.0, 500.0); + test_img_hash(ds, first_run, test_strs[id], 0x59881A79); + } + break; + + case SetFillStyleRGBA: + { + d2d_setfillstylergba(ds, 0xFF00FF20); + d2d_fillrect(ds, 50.0, 50.0, 500.0, 500.0); + test_img_hash(ds, first_run, test_strs[id], 0xA80B06C2); + } + break; + + case SetStrokeStyle: + { + d2d_setstrokestyle(ds, "green"); + d2d_strokerect(ds, 50.0, 50.0, 500.0, 500.0); + test_img_hash(ds, first_run, test_strs[id], 0x3FCD6506); + } + break; + + case SetFillStyle: + { + d2d_setfillstyle(ds, "green"); + d2d_fillrect(ds, 50.0, 50.0, 500.0, 500.0); + test_img_hash(ds, first_run, test_strs[id], 0x6376FAF2); + } + break; + + case SetFont: + { + const char* font = "48px serif"; + d2d_setfont(ds, font); + char out[15] = {0}; + d2d_getcanvaspropstring(ds, "font", out, 15); + if (first_run) { + if (strcmp(font, out) == 0) { + printf("%s test was successful!\n", test_strs[id]); + } else { + printf("%s test failed! Expected %s got %s\n", test_strs[id], font, out); + } + } + } + break; + + case SetLineCap: + { + d2d_setlinecap(ds, "round"); + d2d_setlinewidth(ds, 50.0); + d2d_beginpath(ds); + d2d_moveto(ds, 50.0, 50.0); + d2d_lineto(ds, 100.0, 100.0); + d2d_stroke(ds); + test_img_hash(ds, first_run, test_strs[id], 0x2F86CAF2); + } + break; + + case SetLineJoin: + { + d2d_setlinejoin(ds, "round"); + d2d_setlinewidth(ds, 25.0); + d2d_beginpath(ds); + d2d_moveto(ds, 50.0, 50.0); + d2d_lineto(ds, 100.0, 100.0); + d2d_lineto(ds, 150.0, 50.0); + d2d_stroke(ds); + test_img_hash(ds, first_run, test_strs[id], 0xDEB22C13); + } + break; + + case SetLineDash: + { + const double segments[6] = {10, 20, 15, 30, 20, 40}; + d2d_setlinedash(ds, 6, segments); + d2d_moveto(ds, 50.0, 50.0); + d2d_lineto(ds, 400.0, 400.0); + d2d_stroke(ds); + test_img_hash(ds, first_run, test_strs[id], 0x58E1E91B); + } + break; + + case GetLineDashLength: + { + #define seg_len 6 + const double segments[seg_len] = {10, 20, 15, 30, 20, 40}; + d2d_setlinedash(ds, seg_len, segments); + long len = d2d_getlinedashlength(ds); + if (first_run) { + if (len == seg_len) { + printf("GetLineDashLength test was successful!\n"); + } else { + printf("GetLineDashLength test failed! Expected %ld got %ld\n", (long)seg_len, len); + } + } + } + break; + + case GetLineDash: + { + #define seg_len 6 + const double segments[seg_len+1] = {10, 20, 15, 30, 20, 40, -1.0}; + d2d_setlinedash(ds, seg_len, segments); + + double seg_buffer[seg_len+1]; + seg_buffer[seg_len] = -1.0; //extra segment ensures that the lengths match too + d2d_getlinedash(ds, seg_len+1, seg_buffer); + + bool correct = true; + for (int i = 0; i < seg_len+1; i++) { + if (segments[i] != seg_buffer[i]) { + correct = false; + } + } + if (first_run) { + if (correct) { + printf("GetLineDash test was successful!\n"); + } else { + printf("GetLineDash test failed! Expected {"); + for (int i = 0; i < seg_len+1; i++) { + if (i != 0) printf(", "); + printf("%f", segments[i]); + } + printf("} got {"); + for (int i = 0; i < seg_len+1; i++) { + if (i != 0) printf(", "); + printf("%f", seg_buffer[i]); + } + printf("}\n"); + } + } + + } + break; + + case SetLineDashOffset: + { + const double segments[2] = {20.0, 20.0}; + d2d_setlinedash(ds, 2, segments); + d2d_setlinedashoffset(ds, 10); + d2d_beginpath(ds); + d2d_moveto(ds, 0.0, 0.0); + d2d_lineto(ds, width, height); + d2d_stroke(ds); + test_img_hash(ds, first_run, test_strs[id], 0x5E654AEA); + } + break; + + case BeginPathAndRectAndFill: + { + d2d_beginpath(ds); + d2d_rect(ds, 50.0, 50.0, 500.0, 500.0); + d2d_fill(ds); + test_img_hash(ds, first_run, test_strs[id], 0x259D8A55); + } + break; + + case RectAndStroke: + { + d2d_beginpath(ds); + d2d_rect(ds, 50.0, 50.0, 500.0, 500.0); + d2d_stroke(ds); + test_img_hash(ds, first_run, test_strs[id], 0xD617CFBE); + } + break; + + case MoveToAndLineTo: + { + d2d_beginpath(ds); + d2d_moveto(ds, 50.0, 50.0); + d2d_lineto(ds, 300.0, 50.0); + d2d_stroke(ds); + test_img_hash(ds, first_run, test_strs[id], 0x39277955); + } + break; + + case Arc: + { + d2d_beginpath(ds); + d2d_arc(ds, 50.0, 50.0, 40.0, 0.0, 3.1415*1.5, false); + d2d_stroke(ds); + test_img_hash(ds, first_run, test_strs[id], 0x3B521B43); + } + break; + + case ArcTo: + { + d2d_beginpath(ds); + d2d_moveto(ds, 0.0, 0.0); + d2d_arcto(ds, width/2.0, 250.0, width, 0.0, 400.0); + d2d_stroke(ds); + test_img_hash(ds, first_run, test_strs[id], 0x5039DA68); + } + break; + + case BezierTo: + { + d2d_beginpath(ds); + d2d_moveto(ds, 0.0, 0.0); + d2d_bezierto(ds, 50.0, 100.0, width-75, 250.0, width, 0.0); + d2d_stroke(ds); + test_img_hash(ds, first_run, test_strs[id], 0x7FD136E1); + } + break; + + case RoundRect: + { + d2d_beginpath(ds); + d2d_roundrect(ds, 50.0, 50.0, 500.0, 500.0, 30.0); + d2d_fill(ds); + test_img_hash(ds, first_run, test_strs[id], 0x88068679); + } + break; + + case Ellipse: + { + d2d_beginpath(ds); + d2d_ellipse(ds, 100.0, 100.0, 20.0, 40.0, 3.1415*0.25, 0, 3.1415*2.0, false); + d2d_fill(ds); + test_img_hash(ds, first_run, test_strs[id], 0x41492067); + } + break; + + case QuardraticCurveTo: + { + d2d_beginpath(ds); + d2d_moveto(ds, 0.0, 0.0); + d2d_quadraticcurveto(ds, 250.0, 250.0, width, 0.0); + d2d_stroke(ds); + test_img_hash(ds, first_run, test_strs[id], 0x89C55B66); + } + break; + + case ClosePath: + { + d2d_beginpath(ds); + d2d_moveto(ds, 75.0, 50.0); + d2d_lineto(ds, 100.0, 100.0); + d2d_lineto(ds, 50.0, 100.0); + d2d_closepath(ds); + d2d_fill(ds); + test_img_hash(ds, first_run, test_strs[id], 0x7243A8E2); + } + break; + + case ClearRect: + { + d2d_fillrect(ds, 50.0, 50.0, 500.0, 500.0); + d2d_clearrect(ds, 100.0, 100.0, 400.0, 400.0); + test_img_hash(ds, first_run, test_strs[id], 0xA4E118B3); + } + break; + + case Scale: + { + d2d_scale(ds, 10.0, 15.0); + d2d_fillrect(ds, 10.0, 10.0, 30.0, 30.0); + test_img_hash(ds, first_run, test_strs[id], 0x54E5E8DB); + } + break; + + case Translate: + { + d2d_translate(ds, 50.0, 50.0); + d2d_fillrect(ds, 0.0, 0.0, 500.0, 500.0); + test_img_hash(ds, first_run, test_strs[id], 0x259D8A55); + } + break; + + case Rotate: + { + d2d_rotate(ds, 50.0/180.0 * 3.1415); + d2d_fillrect(ds, 250.0, 25.0, 50.0, 50.0); + test_img_hash(ds, first_run, test_strs[id], 0xB3C72BD5); + } + break; + + case GetTransform: + { + #define transform_len 3 + struct d2d_2d_matrix transforms[transform_len]; + + d2d_gettransform(ds, &transforms[0]); + + d2d_translate(ds, 10.0, 500.0); + d2d_gettransform(ds, &transforms[1]); + + d2d_rotate(ds, 3.1415 * 1.5); + d2d_gettransform(ds, &transforms[2]); + + test_hash(first_run, "GetTransform", 0x1996F72C, crc32((char*)transforms, sizeof(struct d2d_2d_matrix)*transform_len)); + } + break; + + case SetTransform: + { + d2d_settransform(ds, 2.5, 10.0, 10.0, 2.0, 50.0, 50.0); + d2d_fillrect(ds, 0.0, 0.0, 50.0, 50.0); + test_img_hash(ds, first_run, test_strs[id], 0x0526828F); + } + break; + + case Transform: + { + d2d_transform(ds, 2.5, 10.0, 10.0, 2.0, 50.0, 50.0); + d2d_transform(ds, 1.0, 0.0, 0.0, 0.5, 0.0, 0.0); + d2d_fillrect(ds, 0.0, 0.0, 50.0, 50.0); + test_img_hash(ds, first_run, test_strs[id], 0x17EA9B8F); + } + break; + + case ResetTransform: + { + d2d_translate(ds, 100.0, 100.0); + d2d_resettransform(ds); + d2d_fillrect(ds, 0.0, 0.0, 50.0, 50.0); + test_img_hash(ds, first_run, test_strs[id], 0x4A5BCCEB); + } + break; + + case CreateLinearGradient: + { + d2d_createlineargradient(ds, 1, 100.0, 100.0, 450.0, 450.0); + d2d_addcolorstop(ds, 1, 0, "green"); + d2d_addcolorstop(ds, 1, 1, "cyan"); + d2d_setfillstylegradient(ds, 1); + d2d_fillrect(ds, 50.0, 50.0, 500.0, 500.0); + d2d_releaseid(ds, 1); + + test_img_hash(ds, first_run, test_strs[id], 0xF786D7FA); + } + break; + + case CreateRadialGradient: + { + d2d_createradialgradient(ds, 1, 100.0, 100.0, 50.0, 450.0, 450.0, 100.0); + d2d_addcolorstop(ds, 1, 0, "green"); + d2d_addcolorstop(ds, 1, 1, "cyan"); + d2d_setfillstylegradient(ds, 1); + d2d_fillrect(ds, 50.0, 50.0, 500.0, 500.0); + d2d_releaseid(ds, 1); + + test_img_hash(ds, first_run, test_strs[id], 0x44CD7BC4); + } + break; + + case CToImageDataAndPutImageData: + { + #define base_width 3 + #define base_height 3 + #define base_area base_width*base_height + const long base_scale = 64; + const long img_width = base_width*base_scale; + const long img_height = base_height*base_scale; + const long img_area = img_width*img_height; + + const unsigned long base_img[base_area] = { + 0xFFFF0000, 0xFF00FF00, 0xFF0000FF, + 0xFF00FF00, 0xFF0000FF, 0xFFFF0000, + 0xFF0000FF, 0xFFFF0000, 0xFF00FF00, + }; + unsigned long* img_data = malloc(img_area * 4); + for (int b_y = 0; b_y < base_height; b_y++) { + for (int s_y = 0; s_y < base_scale; s_y++) { + int img_y = (b_y*base_scale + s_y) * img_width; + for (int b_x = 0; b_x < base_width; b_x++) { + for (int s_x = 0; s_x < base_scale; s_x++) { + int img_x = b_x*base_scale + s_x; + img_data[img_y + img_x] = base_img[b_y*base_width + b_x]; + } + } + } + } + + d2d_ctoimagedata(ds, 1, (void*)img_data, img_area*4, img_width, img_height); + d2d_putimagedata(ds, 1, 100, 100); + d2d_releaseid(ds, 1); + + test_img_hash(ds, first_run, test_strs[id], 0xD755D8A9); + free(img_data); + } + break; + + case LoadAndDrawImage: + { + #ifdef ASYNC + d2d_load_image("./test-img.jpg", 1); + d2d_drawimage(ds, 1, 0.0, 0.0); + d2d_releaseid(ds, 1); + test_img_hash(ds, first_run, test_strs[id], 0xF35DC5F0); + #else + printf("LoadAndDrawImage test can only be tested with async\n"); + #endif + } + break; + + case LoadAndDrawCroppedImage: + { + #ifdef ASYNC + d2d_load_image("./test-img.jpg", 1); + d2d_drawimage_ex(ds, 1, 450.0, 200.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0); + d2d_releaseid(ds, 1); + test_img_hash(ds, first_run, test_strs[id], 0xDE176A6E); + #else + printf("LoadAndDrawImage test can only be tested with async\n"); + #endif + } + break; + + case GetCanvasPropDouble: + { + const double line_width = 100.0; + d2d_setlinewidth(ds, line_width); + double ret = d2d_getcanvaspropdouble(ds, "lineWidth"); + if (first_run) { + if (ret == line_width) { + printf("GetCanvasPropDouble test was successful!\n"); + } else { + printf("GetCanvasPropDouble test failed! Expected %f got %f\n", line_width, ret); + } + } + } + break; + + case GetCanvasPropString: + { + const char* line_join = "miter"; + d2d_setlinejoin(ds, line_join); + char ret[7]; + d2d_getcanvaspropstring(ds, "lineJoin", ret, 7); + if (first_run) { + if (strcmp(line_join, ret) == 0) { + printf("GetCanvasPropString test was successful!\n"); + } else { + printf("GetCanvasPropString test failed! Expected %s got %s\n", line_join, ret); + } + } + } + break; + + case SetCanvasPropDouble: + { + d2d_setcanvaspropdouble(ds, "lineWidth", 100.0); + + d2d_strokerect(ds, 50.0, 50.0, 500.0, 500.0); + + test_img_hash(ds, first_run, test_strs[id], 0x9EB6F9B6); + } + break; + + case SetCanvasPropString: + { + d2d_setcanvaspropstring(ds, "fillStyle", "#FFF0A0FF"); + + d2d_fillrect(ds, 50.0, 50.0, 500.0, 500.0); + + test_img_hash(ds, first_run, test_strs[id], 0xAEA18583); + } + break; + } + + d2d_end_draw_sequence(ds); + + if (first_run) { + size_t last_avail = avail(); + test_case(id, false); + size_t current_avail = avail(); + + if (last_avail != current_avail) { + printf("%s test had a memory leak! %ld != %ld\n", test_strs[id], last_avail, current_avail); + } + } +} +void test_all() { + for (int test_id = START_TEST; test_id <= END_TEST; test_id++) { + test_case(test_id, true); + } +} + +void test_specific(int id) { + test_case(id+START_TEST, true); +} + +int get_num_tests() { + return END_TEST - START_TEST; +} + +const char* get_test_name(int id) { + return test_strs[id+START_TEST]; +} \ No newline at end of file diff --git a/examples/tests-timer/Makefile b/examples/tests-timer/Makefile new file mode 100644 index 00000000..5e064127 --- /dev/null +++ b/examples/tests-timer/Makefile @@ -0,0 +1,40 @@ + +# tested with mingw32-make using windows + +CC := clang + +# -nostdinc Disable standard #include directories, provided by twr-wasm +# -nostdlib Disable standard c library, provided by twr-wasm +TWRCFLAGS := --target=wasm32 -nostdinc -nostdlib -isystem ../../include + +# -O0 Optimization off (default if no -O specified) +# -O3 Optimization level 3 +# -Wall Warn all +# -c compile w/o linking +# -g for debug symbols (also good to use twrd.a -- debug twr.a lib -- and optimization off -- in this case) +# -v verbose +CFLAGS := -c -Wall -O3 $(TWRCFLAGS) +CFLAGS_DEBUG := -c -Wall -g -O0 $(TWRCFLAGS) + +.PHONY: all + +all: tests-timer.wasm tests-timer-a.wasm + +tests-timer.o: tests-timer.c + $(CC) $(CFLAGS) $< -o $@ + +tests-timer.wasm: tests-timer.o + wasm-ld tests-timer.o ../../lib-c/twr.a -o tests-timer.wasm \ + --no-entry --initial-memory=131072 --max-memory=131072 \ + --export=tests_timer + +tests-timer-a.wasm: tests-timer.o + wasm-ld tests-timer.o ../../lib-c/twr.a -o tests-timer-a.wasm \ + --no-entry --shared-memory --no-check-features --initial-memory=131072 --max-memory=131072 \ + --export=tests_timer + +clean: + rm -f *.o + rm -f *.wasm + + \ No newline at end of file diff --git a/examples/tests-timer/index.html b/examples/tests-timer/index.html new file mode 100644 index 00000000..06d4f5ec --- /dev/null +++ b/examples/tests-timer/index.html @@ -0,0 +1,41 @@ + + + + twr-wasm timer tests + + +
+ + + + + + + + \ No newline at end of file diff --git a/examples/tests-timer/package.json b/examples/tests-timer/package.json new file mode 100644 index 00000000..992479bb --- /dev/null +++ b/examples/tests-timer/package.json @@ -0,0 +1,11 @@ +{ + "@parcel/resolver-default": { + "packageExports": true + }, + "alias": { + "twr-wasm": "../../lib-js/index.js" + }, + "dependencies": { + "twr-wasm": "^2.5.0" + } +} diff --git a/examples/tests-timer/tests-timer.c b/examples/tests-timer/tests-timer.c new file mode 100644 index 00000000..f1bbe443 --- /dev/null +++ b/examples/tests-timer/tests-timer.c @@ -0,0 +1,50 @@ +#include +#include + +int t1_count=0; +int t2_count=0; +int t2_id; + +// timer1 event callback (called once) +__attribute__((export_name("on_timer1"))) +void on_timer1(int event_id) { + t1_count++; + printf("timer callback 1 entered (event id=%d, count=%d)\n", event_id, t1_count); +} + +// timer2 event callback (called multiple times) +__attribute__((export_name("on_timer2"))) +void on_timer2(int event_id) { + t2_count++; + printf("timer callback 2 entered (event id=%d, count=%d)\n", event_id, t2_count); + + if (t2_count==5) { + twr_timer_cancel(t2_id); + if (t1_count!=1) + printf("twr_timer_single_shot FAIL!!\n"); + printf("timer test complete\n"); + } +} + +int tests_timer(int is_async) { + + printf("starting timer tests.\n"); + + int t1_eventid=twr_register_callback("on_timer1"); + twr_timer_single_shot(2000, t1_eventid); + + int t2_eventid=twr_register_callback("on_timer2"); + t2_id=twr_timer_repeat(500, t2_eventid); + + if (is_async) { + printf("going to sleep...\n"); + twr_sleep(1000); + printf("awake from sleep!\n"); + } + + if (t2_count!=0 && t1_count!=0) { + printf("FAIL!! t1_count is %d, t2 %d\n", t1_count, t2_count); + } + + return 0; +} \ No newline at end of file diff --git a/examples/tests-user/tests-user.cpp b/examples/tests-user/tests-user.cpp index 93e11a8b..a252af13 100644 --- a/examples/tests-user/tests-user.cpp +++ b/examples/tests-user/tests-user.cpp @@ -82,7 +82,7 @@ static int unittests(parseCommand) { // printf("malloc_unit_test: %s\n", malloc_unit_test()?"success":"FAIL"); doesn't work here -- see example/tests printf("locale_unit_test: %s\n", locale_unit_test()?"success":"FAIL"); printf("rand_unit_test: %s\n", rand_unit_test()?"success":"FAIL"); - printf("stdlib_unit_test: %s\n", stdlib_unit_test()?"success":"FAIL"); + printf("misc_unit_test: %s\n", misc_unit_test()?"success":"FAIL"); printf("cvtint_unit_test: %s\n", cvtint_unit_test()?"success":"FAIL"); printf("cvtfloat_unit_test: %s\n", cvtfloat_unit_test()?"success":"FAIL"); printf("fcvt_unit_test: %s\n", fcvt_unit_test()?"success":"FAIL"); diff --git a/examples/tests/Makefile b/examples/tests/Makefile index e293d84d..879fbb7e 100644 --- a/examples/tests/Makefile +++ b/examples/tests/Makefile @@ -16,9 +16,9 @@ TWRCFLAGS := --target=wasm32 -nostdinc -nostdlib -isystem ../../include CFLAGS := -c -Wall -O3 $(TWRCFLAGS) CFLAGS_DEBUG := -c -Wall -g -O0 $(TWRCFLAGS) -.PHONY: both +.PHONY: all -both: tests.wasm tests-dbg.wasm +all: tests.wasm tests-dbg.wasm tests-dbg-a.wasm tests.o: tests.c $(CC) $(CFLAGS) $< -o $@ @@ -36,6 +36,11 @@ tests-dbg.wasm: tests-dbg.o --no-entry --initial-memory=131072 --max-memory=131072 \ --export=tests --export=sin_test +tests-dbg-a.wasm: tests-dbg.o + wasm-ld tests.o ../../lib-c/twrd.a -o tests-dbg-a.wasm \ + --no-entry --shared-memory --no-check-features --initial-memory=131072 --max-memory=131072 \ + --export=tests --export=sin_test + clean: rm -f *.o rm -f *.wasm diff --git a/examples/tests/index.html b/examples/tests/index.html index 8792f908..caf0560a 100644 --- a/examples/tests/index.html +++ b/examples/tests/index.html @@ -16,23 +16,31 @@ diff --git a/examples/tests/tests.c b/examples/tests/tests.c index 0c58dc39..c5e28472 100644 --- a/examples/tests/tests.c +++ b/examples/tests/tests.c @@ -14,7 +14,8 @@ int tests() { printf("malloc_unit_test: %s\n", malloc_unit_test()?"success":"FAIL"); printf("locale_unit_test: %s\n", locale_unit_test()?"success":"FAIL"); printf("rand_unit_test: %s\n", rand_unit_test()?"success":"FAIL"); - printf("stdlib_unit_test: %s\n", stdlib_unit_test()?"success":"FAIL"); + printf("math_unit_test: %s\n", math_unit_test()?"success":"FAIL"); + printf("misc_unit_test: %s\n", misc_unit_test()?"success":"FAIL"); printf("cvtint_unit_test: %s\n", cvtint_unit_test()?"success":"FAIL"); printf("cvtfloat_unit_test: %s\n", cvtfloat_unit_test()?"success":"FAIL"); printf("fcvt_unit_test: %s\n", fcvt_unit_test()?"success":"FAIL"); @@ -23,6 +24,7 @@ int tests() { printf("string_unit_test: %s\n", string_unit_test()?"success":"FAIL"); printf("printf_unit_test: %s\n", printf_unit_test()?"success":"FAIL"); printf("mbstring_unit_test: %s\n", mbstring_unit_test()?"success":"FAIL"); + printf("test run complete\n"); return 0; diff --git a/examples/twr-cpp/canvas.cpp b/examples/twr-cpp/canvas.cpp index 7ccbef66..dd81169d 100644 --- a/examples/twr-cpp/canvas.cpp +++ b/examples/twr-cpp/canvas.cpp @@ -122,9 +122,12 @@ void twrCanvas::strokeText(const char* str, double x, double y) { d2d_stroketext(m_ds, str, x, y); } +void twrCanvas::imageDataToC(long id, void* mem, unsigned long length, unsigned long width, unsigned long height) { + assert(m_ds); + d2d_imagedata(m_ds, id, mem, length, width, height); +} void twrCanvas::imageData(long id, void* mem, unsigned long length, unsigned long width, unsigned long height) { - assert(m_ds); - d2d_imagedata(m_ds, id, mem, length, width, height); + this->imageDataToC(id, mem, length, width, height); } void twrCanvas::putImageData(long id, unsigned long dx, unsigned long dy) { @@ -272,6 +275,10 @@ void twrCanvas::drawImage(long id, double dx, double dy) { assert(m_ds); d2d_drawimage(m_ds, id, dx, dy); } +void twrCanvas::drawImage(long id, double sx, double sy, double sWidth, double sHeight, double dx, double dy, double dWidth, double dHeight) { + assert(m_ds); + d2d_drawimage_ex(m_ds, id, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); +} void twrCanvas::rect(double x, double y, double width, double height) { assert(m_ds); @@ -302,10 +309,32 @@ void twrCanvas::setLineDashOffset(double line_dash_offset) { d2d_setlinedashoffset(m_ds, line_dash_offset); } -void twrCanvas::getImageData(double x, double y, double width, double height, void* buffer, unsigned long buffer_len) { +void twrCanvas::getImageData(long id, double x, double y, double width, double height) { assert(m_ds); - d2d_getimagedata(m_ds, x, y, width, height, buffer, buffer_len); + d2d_getimagedata(m_ds, id, x, y, width, height); } unsigned long twrCanvas::getImageDataSize(double width, double height) { return d2d_getimagedatasize(width, height); +} + +void twrCanvas::imageDataToC(long id, void* buffer, unsigned long buffer_len) { + assert(m_ds); + d2d_imagedatatoc(m_ds, id, buffer, buffer_len); +} + +double twrCanvas::getCanvasPropDouble(const char* prop_name) { + assert(m_ds); + return d2d_getcanvaspropdouble(m_ds, prop_name); +} +void twrCanvas::getCanvasPropString(const char* prop_name, char* buffer, unsigned long buffer_len) { + assert(m_ds); + d2d_getcanvaspropstring(m_ds, prop_name, buffer, buffer_len); +} +void twrCanvas::setCanvasPropDouble(const char* prop_name, double val) { + assert(m_ds); + d2d_setcanvaspropdouble(m_ds, prop_name, val); +} +void twrCanvas::setCanvasPropString(const char* prop_name, const char* val) { + assert(m_ds); + d2d_setcanvaspropstring(m_ds, prop_name, val); } \ No newline at end of file diff --git a/examples/twr-cpp/canvas.h b/examples/twr-cpp/canvas.h index c95b6216..7b5416e5 100644 --- a/examples/twr-cpp/canvas.h +++ b/examples/twr-cpp/canvas.h @@ -1,9 +1,13 @@ +#ifndef TWR_CPP_CANVAS_H +#define TWR_CPP_CANVAS_H + #include #include "twr-draw2d.h" ////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////// + typedef unsigned long colorRGB_t; typedef unsigned long colorRGBA_t; @@ -62,8 +66,10 @@ class twrCanvas { void fillText(const char* str, double x, double y); void fillCodePoint(unsigned long c, double x, double y); void strokeText(const char* str, double x, double y); - + + //depreciated used cToImageData instead void imageData(long id, void* mem, unsigned long length, unsigned long width, unsigned long height); + void imageDataToC(long id, void* mem, unsigned long length, unsigned long width, unsigned long height); void putImageData(long id, unsigned long dx, unsigned long dy); void putImageData(long id, unsigned long dx, unsigned long dy, unsigned long dirtyX, unsigned long dirtyY, unsigned long dirtyWidth, unsigned long dirtyHeight); @@ -83,9 +89,19 @@ class twrCanvas { unsigned long getLineDashLength(); void drawImage(long id, double dx, double dy); - void getImageData(double x, double y, double width, double height, void* buffer, unsigned long buffer_len); + void drawImage(long id, double sx, double sy, double sWidth, double sHeight, double dx, double dy, double dWidth, double dHeight); + void getImageData(long id, double x, double y, double width, double height); unsigned long getImageDataSize(double width, double height); + void imageDataToC(long id, void* buffer, unsigned long buffer_len); + + double getCanvasPropDouble(const char* prop_name); + void getCanvasPropString(const char* prop_name, char* buffer, unsigned long buffer_len); + void setCanvasPropDouble(const char* prop_name, double val); + void setCanvasPropString(const char* prop_name, const char* val); + private: struct d2d_draw_seq *m_ds; }; + +#endif \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 1cc10ead..701a697f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,6 +29,7 @@ nav: - Basic Steps: gettingstarted/basicsteps.md - Passing Arguments: gettingstarted/parameters.md - Consoles: gettingstarted/stdio.md + - Events: gettingstarted/events.md - Character Encoding: gettingstarted/charencoding.md - Debugging: gettingstarted/debugging.md - Compiler, Linker, Memory: gettingstarted/compiler-opts.md @@ -43,15 +44,21 @@ nav: - pong: examples/examples-pong.md - maze: examples/examples-maze.md - callC: examples/examples-callc.md + - library: examples/examples-lib.md - libc++: examples/examples-libcxx.md - - API Reference: - - TypeScript/JavaScript: api/api-typescript.md - - C Localization: api/api-localization.md - - C General: api/api-c-general.md - - C++ with libc++: api/api-libcpp.md - - C Draw 2D: api/api-c-d2d.md - - C Console Char I/O: api/api-c-con.md - - C Standard C Library: api/api-c-stdlib.md + - C/C++ API: + - General: api/api-c-general.md + - Localization: api/api-c-localization.md + - Draw 2D: api/api-c-d2d.md + - Console Char I/O: api/api-c-con.md + - Standard C Library: api/api-c-stdlib.md + - libc++: api/api-libcpp.md + - Audio: api/api-c-audio.md + - TypeScript API: + - Modules: api/api-ts-modules.md + - Consoles: api/api-ts-consoles.md + - Memory: api/api-ts-memory.md + - Libraries: api/api-ts-library.md - More: - Wasm Runtime Limitations: more/wasm-problem.md - Import Resolution: more/imports.md diff --git a/notes-to-self.txt b/notes-to-self.txt index e190e2f7..1b5b255f 100644 --- a/notes-to-self.txt +++ b/notes-to-self.txt @@ -14,6 +14,7 @@ change build all examples scripts to also rebuild the libraries make files consider creating a sysroot -- see https://danielmangum.com/posts/wasm-wasi-clang-17/ ** docs/examples ** +rename kiss readme(or remove) in example fft. Right now, it shows up as the primary readme on github in the fft example source for parameter.md, add an explanation of bool type for parameter.md, provide more detail in the js object section, and add working examples to call.c example. when the examples/index is running locally, the link View complete docs does not work @@ -97,6 +98,8 @@ malloc_unit_test() can't be called twice because cache mem tests don't clean up. some of the length printf tests are failing with optimization on. Why? ** existing API implementation improvements ** +add the ability to allow await to operate on a twr-wasm C function? (implement promises) +bug in divcon -- long text doesn't wrap correctly. move twrConDrawSeq into jscon driver add buffer size to io_mbgets(stream1, buffer); should getProxyParams() bet getProxyArgs() ? @@ -139,7 +142,6 @@ change canvas cursor (hollow) when canvas doesn't have focus add check of response.ok to fetchAndPutURL 0 fetch (see loadWasm) io_putc() used unsigned char, which will fail with EOF, and unicode code points (should i add that?) bug: setlocale(LC_ALL, NULL) should return different locals separated by semicolon. see https://chatgpt.com/c/30a0e4f7-8e04-427c-9943-950e74633292 -rename nstrcopy to __nstrcopy Add tests for utf-32/16/etc and add to docs -- see https://chatgpt.com/c/30a0e4f7-8e04-427c-9943-950e74633292 ** os notes - ideas ** @@ -166,6 +168,8 @@ UUID_Array=findFiles("tag=board-games"); returns file=open(UUID_Array[0]) ** misc ** +change as many 'any' to 'unknown' as possible to increase type safety +finish TODO in twrcon.ts remove gcc workspace from git after github clone, the following errors are encountred running the examples: Could not read source map for file:///C:/GitHubClones3rdParty/twr-wasm/examples/maze/maze-script.js: ENOENT: no such file or directory, open 'c:\GitHubClones3rdParty\twr-wasm\examples\maze\maze-script.js.map' diff --git a/package.json b/package.json index b0453a90..3132ef60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "twr-wasm", - "version": "2.4.2", + "version": "2.5.0", "description": "twr-wasm provides a simple way to run C/C++ code in a web browser using WebAssembly.", "keywords": [ "wasm", diff --git a/readme.md b/readme.md index 8b8880b7..cc89b6cf 100644 --- a/readme.md +++ b/readme.md @@ -1,17 +1,25 @@ # Easier C/C++ WebAssembly -**Version 2.4.2** - -twr-wasm is a simple, lightweight and easy to use library for building C/C++ WebAssembly code directly with clang. It solves some common use cases with less work than the more feature rich emscripten. - -twr-wasm includes comprehensive console support for `stdio`. You can input and print to a `
` tag, or use a `` element as an terminal. - -twr-wasm makes it easy to `await` on blocking C/C++ functions. - -twr-wasm makes it easy to use C/C++ 2D drawing apis that are compatible with JavaScript Canvas APIs to draw to a `` element. - -twr-wasm allows you to run C/C++ code in a web browser. Legacy code, libraries, full applications, or single functions can be integrated with JavaScript and TypeScript. - -twr-wasm is designed to be used with the standard llvm clang compiler and tools. +**Version 2.5.0** + +twr-wasm is a simple, lightweight and easy to use library for building C/C++ WebAssembly code directly with clang. Run C/C++ code in a web browser. Legacy code, libraries, full applications, or single functions can be integrated with JavaScript and TypeScript. twr-wam solves some common use cases with less work than the more feature rich emscripten. + +**Key Features:** + +- build `.wasm` modules using C/C++ using clang directly (no wrapper) +- from JavaScript load `.wasm` modules, call C/C++ functions, and access wasm memory +- comprehensive console support for `stdin`, `stdio`, and `stderr`. + - in C/C++, print and get characters to/from `
` tags in your HTML page + - in C/C++, print and get characters to/from a `` based "terminal" + - localization support, UTF-8, and windows-1252 support +- the optional TypeScript `class twrWasmModuleAsync` can be used to: + - integrate CLI C/C++ code with JavaScript + - In JavaScript `await` on blocking/synchronous C/C++ functions. +- 2D drawing API for C/C++ compatible with JavaScript Canvas +- audio playback APIs for C/C++ +- create your own C/C++ APIs using TypeScript by extending `class twrLibrary` +- standard C library optimized for WebAssembly +- libc++ built for WebAssembly +- comprehensive examples and documentation ## Live WebAssembly Examples and Source @@ -26,16 +34,6 @@ twr-wasm is designed to be used with the standard llvm clang compiler and tools. ## Full Documentation The full documentation can be [found here](https://twiddlingbits.dev/docsite/) -## Key Features - - compile and link C/C++ for use with WebAssembly using clang directly - - standard C library, libc++. and purpose built APIs available from C/C++ - - TypeScrpt/JavaScript classes to load Wasm modules and call C/C++ functions - - localization support, UTF-8, and windows-1252 support - - in C/C++, print and get characters to/from `
` tags in your HTML page - - in C/C++, print and get characters to/from a `` based "terminal" - - in C/C++ use 2D drawing API compatible with JavaScript Canvas - - in C/C++, use the "blocking loop" pattern and integrate with Javascript's asynchronous event loop - ## Installation `npm install twr-wasm`. diff --git a/server.py b/server.py new file mode 100644 index 00000000..5ba249b2 --- /dev/null +++ b/server.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +from http import server # Python 3 + +class MyHTTPRequestHandler(server.SimpleHTTPRequestHandler): + def end_headers(self): + self.send_my_headers() + server.SimpleHTTPRequestHandler.end_headers(self) + + def send_my_headers(self): + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Cross-Origin-Embedder-Policy", "require-corp") + self.send_header("Cross-Origin-Opener-Policy", "same-origin") + +if __name__ == '__main__': + server.test(HandlerClass=MyHTTPRequestHandler) + \ No newline at end of file diff --git a/source/Makefile b/source/Makefile index b84c9337..5b69f27d 100644 --- a/source/Makefile +++ b/source/Makefile @@ -45,7 +45,7 @@ all: $(TWRA) $(TWRA_DEBUG) libjs # build typescript files libjs: @mkdir -p $(LIBJSOUTDIR) - tsc -p twr-ts/tsconfig.json + tsc -b tsconfig.json # put all objects into a single directory; assumes unqiue filenames OBJECTS_RAW := \ diff --git a/source/tsconfig.json b/source/tsconfig.json new file mode 100644 index 00000000..3e6527a2 --- /dev/null +++ b/source/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./twr-ts" }, + ] + } + \ No newline at end of file diff --git a/source/twr-c/audio.c b/source/twr-c/audio.c new file mode 100644 index 00000000..34a907b9 --- /dev/null +++ b/source/twr-c/audio.c @@ -0,0 +1,40 @@ +#include "twr-audio.h" +#include "twr-jsimports.h" +#include + + + +struct PlayRangeFields twr_audio_default_play_range() { + struct PlayRangeFields fields = { + .finish_callback = -1000, + .loop = false, + .pan = 0.0, + .volume = 1.0, + .sample_rate = 0, + }; + + return fields; +} + +long twr_audio_play_range_ex(long node_id, long start_sample, long end_sample, struct PlayRangeFields* fields) { + __attribute__((import_name("twrAudioPlayRange"))) long twr_audio_play_range_callback(long node_id, long start_sample, long end_sample, int loop, long sample_rate, double volume, double pan, int finish_callback); + + return twr_audio_play_range_callback(node_id, start_sample, end_sample, fields->loop, fields->sample_rate, fields->volume, fields->pan, fields->finish_callback); +} + +struct PlayRangeSyncFields twr_audio_default_play_range_sync() { + struct PlayRangeSyncFields fields = { + .loop = false, + .pan = 0.0, + .volume = 1.0, + .sample_rate = 0 + }; + + return fields; +} + +long twr_audio_play_range_sync_ex(long node_id, long start_sample, long end_sample, struct PlayRangeSyncFields* fields) { + __attribute__((import_name("twrAudioPlayRangeSync"))) long twr_audio_play_range_sync_sample_rate(long node_id, long start_sample, long end_sample, int loop, long sample_rate, double volume, double pan); + + return twr_audio_play_range_sync_sample_rate(node_id, start_sample, end_sample, fields->loop, fields->sample_rate, fields->volume, fields->pan); +} diff --git a/source/twr-c/draw2d.c b/source/twr-c/draw2d.c index bd05c922..beda8735 100644 --- a/source/twr-c/draw2d.c +++ b/source/twr-c/draw2d.c @@ -1,9 +1,9 @@ #include #include #include -#include "twr-jsimports.h" #include "twr-crt.h" #include "twr-draw2d.h" +#include // ceil static twr_ioconsole_t *__std2d; @@ -25,7 +25,10 @@ void d2d_free_instructions(struct d2d_draw_seq* ds) { //twr_conlog("free instruction me %x type %x next %x",next, next->type, next->next); struct d2d_instruction_hdr * nextnext=next->next; if (next->heap_ptr != NULL) { - free(next->heap_ptr); + twr_cache_free(next->heap_ptr); + } + if (next->heap_ptr2 != NULL) { + twr_cache_free(next->heap_ptr2); } twr_cache_free(next); next=nextnext; @@ -35,6 +38,15 @@ void d2d_free_instructions(struct d2d_draw_seq* ds) { } } +char* cache_strdup(const char* str) { + size_t len = strlen(str) + 1; //include null terminator + char* ret = (char*)twr_cache_malloc(len); + + memcpy(ret, str, len); + + return ret; +} + static void invalidate_cache(struct d2d_draw_seq* ds) { ds->last_fillstyle_color_valid=false; ds->last_strokestyle_color_valid=false; @@ -92,7 +104,7 @@ void new_instruction(struct d2d_draw_seq* ds) { } } -static void set_ptrs(struct d2d_draw_seq* ds, struct d2d_instruction_hdr *e, void* heap_ptr) { +static void set_ptrs(struct d2d_draw_seq* ds, struct d2d_instruction_hdr *e, void* heap_ptr, void* heap_ptr2) { assert(ds); if (ds->start==0) { ds->start=e; @@ -103,6 +115,7 @@ static void set_ptrs(struct d2d_draw_seq* ds, struct d2d_instruction_hdr *e, voi ds->last->next=e; ds->last=e; e->heap_ptr = heap_ptr; + e->heap_ptr2 = heap_ptr2; new_instruction(ds); //twr_conlog("C: set_ptrs ds->last set to %x",ds->last); } @@ -118,7 +131,7 @@ void d2d_fillrect(struct d2d_draw_seq* ds, double x, double y, double w, double r->y=y; r->w=w; r->h=h; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); //twr_conlog("C: fillrect,last_fillstyle_color: %d",ds->last_fillstyle_color); } @@ -129,7 +142,7 @@ void d2d_strokerect(struct d2d_draw_seq* ds, double x, double y, double w, doubl r->y=y; r->w=w; r->h=h; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); } void d2d_setlinewidth(struct d2d_draw_seq* ds, double width) { @@ -138,7 +151,7 @@ void d2d_setlinewidth(struct d2d_draw_seq* ds, double width) { struct d2dins_setlinewidth* e= twr_cache_malloc(sizeof(struct d2dins_setlinewidth)); e->hdr.type=D2D_SETLINEWIDTH; e->width=width; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } } @@ -152,7 +165,7 @@ void d2d_setfillstylergba(struct d2d_draw_seq* ds, unsigned long color) { struct d2dins_setfillstylergba* e= twr_cache_malloc(sizeof(struct d2dins_setfillstylergba)); e->hdr.type=D2D_SETFILLSTYLERGBA; e->color=color; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } } @@ -166,60 +179,60 @@ void d2d_setstrokestylergba(struct d2d_draw_seq* ds, unsigned long color) { struct d2dins_setstrokestylergba* e= twr_cache_malloc(sizeof(struct d2dins_setstrokestylergba)); e->hdr.type=D2D_SETSTROKESTYLERGBA; e->color=color; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } } void d2d_setfillstyle(struct d2d_draw_seq* ds, const char* css_color) { struct d2dins_setfillstyle* e= twr_cache_malloc(sizeof(struct d2dins_setfillstyle)); e->hdr.type=D2D_SETFILLSTYLE; - e->css_color=strdup(css_color); - set_ptrs(ds, &e->hdr, (void*)e->css_color); + e->css_color=cache_strdup(css_color); + set_ptrs(ds, &e->hdr, (void*)e->css_color, NULL); } void d2d_setstrokestyle(struct d2d_draw_seq* ds, const char* css_color) { struct d2dins_setstrokestyle* e= twr_cache_malloc(sizeof(struct d2dins_setstrokestyle)); e->hdr.type=D2D_SETSTROKESTYLE; - e->css_color=strdup(css_color); - set_ptrs(ds, &e->hdr, (void*)e->css_color); + e->css_color=cache_strdup(css_color); + set_ptrs(ds, &e->hdr, (void*)e->css_color, NULL); } void d2d_setfont(struct d2d_draw_seq* ds, const char* font) { struct d2dins_setfont* e= twr_cache_malloc(sizeof(struct d2dins_setfont)); e->hdr.type=D2D_SETFONT; - e->font=strdup(font); - set_ptrs(ds, &e->hdr, (void*)e->font); + e->font=cache_strdup(font); + set_ptrs(ds, &e->hdr, (void*)e->font, NULL); } void d2d_beginpath(struct d2d_draw_seq* ds) { struct d2dins_beginpath* e= twr_cache_malloc(sizeof(struct d2dins_beginpath)); e->hdr.type=D2D_BEGINPATH; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_fill(struct d2d_draw_seq* ds) { struct d2dins_fill* e= twr_cache_malloc(sizeof(struct d2dins_fill)); e->hdr.type=D2D_FILL; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_stroke(struct d2d_draw_seq* ds) { struct d2dins_stroke* e= twr_cache_malloc(sizeof(struct d2dins_stroke)); e->hdr.type=D2D_STROKE; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_save(struct d2d_draw_seq* ds) { struct d2dins_save* e= twr_cache_malloc(sizeof(struct d2dins_save)); e->hdr.type=D2D_SAVE; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_restore(struct d2d_draw_seq* ds) { struct d2dins_restore* e= twr_cache_malloc(sizeof(struct d2dins_restore)); invalidate_cache(ds); e->hdr.type=D2D_RESTORE; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_moveto(struct d2d_draw_seq* ds, double x, double y) { @@ -227,7 +240,7 @@ void d2d_moveto(struct d2d_draw_seq* ds, double x, double y) { e->hdr.type=D2D_MOVETO; e->x=x; e->y=y; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_lineto(struct d2d_draw_seq* ds, double x, double y) { @@ -235,7 +248,7 @@ void d2d_lineto(struct d2d_draw_seq* ds, double x, double y) { e->hdr.type=D2D_LINETO; e->x=x; e->y=y; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_arc(struct d2d_draw_seq* ds, double x, double y, double radius, double start_angle, double end_angle, bool counterclockwise) { @@ -247,7 +260,7 @@ void d2d_arc(struct d2d_draw_seq* ds, double x, double y, double radius, double e->start_angle=start_angle; e->end_angle=end_angle; e->counterclockwise=counterclockwise; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_bezierto(struct d2d_draw_seq* ds, double cp1x, double cp1y, double cp2x, double cp2y, double x, double y) { @@ -259,7 +272,7 @@ void d2d_bezierto(struct d2d_draw_seq* ds, double cp1x, double cp1y, double cp2x e->cp2y=cp2y; e->x=x; e->y=y; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } @@ -268,9 +281,9 @@ void d2d_filltext(struct d2d_draw_seq* ds, const char* str, double x, double y) e->hdr.type=D2D_FILLTEXT; e->x=x; e->y=y; - e->str=strdup(str); + e->str=cache_strdup(str); e->code_page=__get_current_lc_ctype_code_page_modified(); - set_ptrs(ds, &e->hdr, (void*)e->str); + set_ptrs(ds, &e->hdr, (void*)e->str, NULL); } // c is a unicode 32 bit codepoint @@ -281,7 +294,7 @@ void d2d_fillcodepoint(struct d2d_draw_seq* ds, unsigned long c, double x, doubl e->y=y; e->c=c; //twr_conlog("C: d2d_char %d %d %d",e->x, e->y, e->c); - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_stroketext(struct d2d_draw_seq* ds, const char* str, double x, double y) { @@ -289,9 +302,9 @@ void d2d_stroketext(struct d2d_draw_seq* ds, const char* str, double x, double y r->hdr.type=D2D_STROKETEXT; r->x=x; r->y=y; - r->str=strdup(str); + r->str=cache_strdup(str); r->code_page=__get_current_lc_ctype_code_page_modified(); - set_ptrs(ds, &r->hdr, (void*)r->str); + set_ptrs(ds, &r->hdr, (void*)r->str, NULL); } // causes a flush so that a result is returned in *tm @@ -303,20 +316,25 @@ void d2d_measuretext(struct d2d_draw_seq* ds, const char* str, struct d2d_text_m e->str=str; e->tm=tm; e->code_page=__get_current_lc_ctype_code_page_modified(); - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); d2d_flush(ds); } //needs to be static or flushed before mem goes out of scope -void d2d_imagedata(struct d2d_draw_seq* ds, long id, void* mem, unsigned long length, unsigned long width, unsigned long height) { - struct d2dins_image_data* e= twr_cache_malloc(sizeof(struct d2dins_image_data)); +void d2d_ctoimagedata(struct d2d_draw_seq* ds, long id, void* mem, unsigned long length, unsigned long width, unsigned long height) { + struct d2dins_c_to_image_data* e= twr_cache_malloc(sizeof(struct d2dins_c_to_image_data)); e->hdr.type=D2D_IMAGEDATA; e->start=mem-(void*)0; e->length=length; e->width=width; e->height=height; e->id=id; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); +} + +//depreciated used d2d_ctoimagedata instead +void d2d_imagedata(struct d2d_draw_seq* ds, long id, void* mem, unsigned long length, unsigned long width, unsigned long height) { + d2d_ctoimagedata(ds, id, mem, length, width, height); } @@ -335,7 +353,7 @@ void d2d_putimagedatadirty(struct d2d_draw_seq* ds, long id, unsigned long dx, u e->dirtyY=dirtyY; e->dirtyWidth=dirtyWidth; e->dirtyHeight=dirtyHeight; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_createradialgradient(struct d2d_draw_seq* ds, long id, double x0, double y0, double radius0, double x1, double y1, double radius1) { @@ -348,7 +366,7 @@ void d2d_createradialgradient(struct d2d_draw_seq* ds, long id, double x0, doubl e->x1=x1; e->y1=y1; e->radius1=radius1; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_createlineargradient(struct d2d_draw_seq* ds, long id, double x0, double y0, double x1, double y1) { @@ -359,7 +377,7 @@ void d2d_createlineargradient(struct d2d_draw_seq* ds, long id, double x0, doubl e->y0=y0; e->x1=x1; e->y1=y1; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_addcolorstop(struct d2d_draw_seq* ds, long gradid, long position, const char* csscolor) { @@ -367,35 +385,35 @@ void d2d_addcolorstop(struct d2d_draw_seq* ds, long gradid, long position, const e->hdr.type=D2D_SETCOLORSTOP; e->id=gradid; e->position=position; - e->csscolor=strdup(csscolor); - set_ptrs(ds, &e->hdr, (void*)e->csscolor); + e->csscolor=cache_strdup(csscolor); + set_ptrs(ds, &e->hdr, (void*)e->csscolor, NULL); } void d2d_setfillstylegradient(struct d2d_draw_seq* ds, long gradid) { struct d2dins_set_fillstyle_gradient* e= twr_cache_malloc(sizeof(struct d2dins_set_fillstyle_gradient)); e->hdr.type=D2D_SETFILLSTYLEGRADIENT; e->id=gradid; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_releaseid(struct d2d_draw_seq* ds, long id) { struct d2dins_release_id* e= twr_cache_malloc(sizeof(struct d2dins_release_id)); e->hdr.type=D2D_RELEASEID; e->id=id; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_closepath(struct d2d_draw_seq* ds) { struct d2dins_closepath* e= twr_cache_malloc(sizeof(struct d2dins_closepath)); e->hdr.type=D2D_CLOSEPATH; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_reset(struct d2d_draw_seq* ds) { invalidate_cache(ds); struct d2dins_reset* e= twr_cache_malloc(sizeof(struct d2dins_reset)); e->hdr.type=D2D_RESET; - set_ptrs(ds, &e->hdr, NULL); + set_ptrs(ds, &e->hdr, NULL, NULL); } void d2d_clearrect(struct d2d_draw_seq* ds, double x, double y, double w, double h) { @@ -405,7 +423,7 @@ void d2d_clearrect(struct d2d_draw_seq* ds, double x, double y, double w, double r->y=y; r->w=w; r->h=h; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); } void d2d_scale(struct d2d_draw_seq* ds, double x, double y) { @@ -413,7 +431,7 @@ void d2d_scale(struct d2d_draw_seq* ds, double x, double y) { r->hdr.type=D2D_SCALE; r->x=x; r->y=y; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); } void d2d_translate(struct d2d_draw_seq* ds, double x, double y) { @@ -421,21 +439,21 @@ void d2d_translate(struct d2d_draw_seq* ds, double x, double y) { r->hdr.type=D2D_TRANSLATE; r->x=x; r->y=y; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); } void d2d_rotate(struct d2d_draw_seq* ds, double angle) { struct d2dins_rotate* r= twr_cache_malloc(sizeof(struct d2dins_rotate)); r->hdr.type=D2D_ROTATE; r->angle=angle; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); } void d2d_gettransform(struct d2d_draw_seq* ds, struct d2d_2d_matrix* transform) { struct d2dins_gettransform* r = twr_cache_malloc(sizeof(struct d2dins_gettransform)); r->hdr.type=D2D_GETTRANSFORM; r->transform = transform; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); d2d_flush(ds); } @@ -448,7 +466,7 @@ void d2d_settransform(struct d2d_draw_seq* ds, double a, double b, double c, dou r->d = d; r->e = e; r->f = f; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); } void d2d_settransformmatrix(struct d2d_draw_seq* ds, const struct d2d_2d_matrix * transform) { d2d_settransform(ds, transform->a, transform->b, transform->c, transform->d, transform->e, transform->f); @@ -457,7 +475,7 @@ void d2d_settransformmatrix(struct d2d_draw_seq* ds, const struct d2d_2d_matrix void d2d_resettransform(struct d2d_draw_seq* ds) { struct d2dins_resettransform* r = twr_cache_malloc(sizeof(struct d2dins_resettransform)); r->hdr.type=D2D_RESETTRANSFORM; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); } void d2d_roundrect(struct d2d_draw_seq* ds, double x, double y, double width, double height, double radii) { @@ -468,7 +486,7 @@ void d2d_roundrect(struct d2d_draw_seq* ds, double x, double y, double width, do r->width = width; r->height = height; r->radii = radii; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); } void d2d_ellipse(struct d2d_draw_seq* ds, double x, double y, double radiusX, double radiusY, double rotation, double startAngle, double endAngle, bool counterclockwise) { @@ -482,7 +500,7 @@ void d2d_ellipse(struct d2d_draw_seq* ds, double x, double y, double radiusX, do r->startAngle = startAngle; r->endAngle = endAngle; r->counterclockwise = counterclockwise; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); } void d2d_quadraticcurveto(struct d2d_draw_seq* ds, double cpx, double cpy, double x, double y) { @@ -492,7 +510,7 @@ void d2d_quadraticcurveto(struct d2d_draw_seq* ds, double cpx, double cpy, doubl r->cpy = cpy; r->x = x; r->y = y; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); } void d2d_setlinedash(struct d2d_draw_seq* ds, unsigned long len, const double* segments) { @@ -501,13 +519,13 @@ void d2d_setlinedash(struct d2d_draw_seq* ds, unsigned long len, const double* s r->segment_len = len; r->segments = NULL; if (len > 0) { - r->segments = malloc(sizeof(double) * len); + r->segments = twr_cache_malloc(sizeof(double) * len); for (int i = 0; i < len; i++) { r->segments[i] = segments[i]; } } - set_ptrs(ds, &r->hdr, (void*)r->segments); + set_ptrs(ds, &r->hdr, (void*)r->segments, NULL); } unsigned long d2d_getlinedash(struct d2d_draw_seq* ds, unsigned long length, double* buffer) { @@ -515,7 +533,7 @@ unsigned long d2d_getlinedash(struct d2d_draw_seq* ds, unsigned long length, dou r->hdr.type = D2D_GETLINEDASH; r->buffer = buffer; r->buffer_length = length; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); d2d_flush(ds); return r->segment_length; } @@ -528,13 +546,13 @@ void d2d_arcto(struct d2d_draw_seq* ds, double x1, double y1, double x2, double r->x2 = x2; r->y2 = y2; r->radius = radius; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); } unsigned long d2d_getlinedashlength(struct d2d_draw_seq* ds) { struct d2dins_getlinedashlength* r = twr_cache_malloc(sizeof(struct d2dins_getlinedashlength)); r->hdr.type = D2D_GETLINEDASHLENGTH; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); d2d_flush(ds); return r->length; } @@ -546,13 +564,24 @@ bool d2d_load_image_with_con(const char* url, long id, twr_ioconsole_t * con) { return twrConLoadImage(__twr_get_jsid(con), url, id); } -void d2d_drawimage(struct d2d_draw_seq* ds, long id, double dx, double dy) { +void d2d_drawimage_ex(struct d2d_draw_seq* ds, long id, double sx, double sy, double sWidth, double sHeight, double dx, double dy, double dWidth, double dHeight) { struct d2dins_drawimage* r = twr_cache_malloc(sizeof(struct d2dins_drawimage)); r->hdr.type = D2D_DRAWIMAGE; r->id = id; + r->sx = sx; + r->sy = sy; + r->sWidth = sWidth; + r->sHeight = sHeight; r->dx = dx; r->dy = dy; - set_ptrs(ds, &r->hdr, NULL); + r->dHeight = dHeight; + r->dWidth = dWidth; + + set_ptrs(ds, &r->hdr, NULL, NULL); +} + +void d2d_drawimage(struct d2d_draw_seq* ds, long id, double dx, double dy) { + d2d_drawimage_ex(ds, id, 0, 0, 0, 0, dx, dy, 0, 0); } void d2d_rect(struct d2d_draw_seq* ds, double x, double y, double width, double height) { @@ -562,7 +591,7 @@ void d2d_rect(struct d2d_draw_seq* ds, double x, double y, double width, double r->y = y; r->width = width; r->height = height; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); } void d2d_transform(struct d2d_draw_seq* ds, double a, double b, double c, double d, double e, double f) { @@ -574,7 +603,7 @@ void d2d_transform(struct d2d_draw_seq* ds, double a, double b, double c, double r->d = d; r->e = e; r->f = f; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); } void d2d_transformmatrix(struct d2d_draw_seq* ds, const struct d2d_2d_matrix * transform) { d2d_transform(ds, transform->a, transform->b, transform->c, transform->d, transform->e, transform->f); @@ -583,35 +612,33 @@ void d2d_transformmatrix(struct d2d_draw_seq* ds, const struct d2d_2d_matrix * t void d2d_setlinecap(struct d2d_draw_seq* ds, const char* line_cap) { struct d2dins_setlinecap* r = twr_cache_malloc(sizeof(struct d2dins_setlinecap)); r->hdr.type = D2D_SETLINECAP; - r->line_cap = strdup(line_cap); - set_ptrs(ds, &r->hdr, (void*)r->line_cap); + r->line_cap = cache_strdup(line_cap); + set_ptrs(ds, &r->hdr, (void*)r->line_cap, NULL); } void d2d_setlinejoin(struct d2d_draw_seq* ds, const char* line_join) { struct d2dins_setlinejoin* r = twr_cache_malloc(sizeof(struct d2dins_setlinejoin)); r->hdr.type = D2D_SETLINEJOIN; - r->line_join = strdup(line_join); - set_ptrs(ds, &r->hdr, (void*)r->line_join); + r->line_join = cache_strdup(line_join); + set_ptrs(ds, &r->hdr, (void*)r->line_join, NULL); } void d2d_setlinedashoffset(struct d2d_draw_seq* ds, double line_dash_offset) { struct d2dins_setlinedashoffset* r = twr_cache_malloc(sizeof(struct d2dins_setlinedashoffset)); r->hdr.type = D2D_SETLINEDASHOFFSET; r->line_dash_offset = line_dash_offset; - set_ptrs(ds, &r->hdr, NULL); + set_ptrs(ds, &r->hdr, NULL, NULL); } -void d2d_getimagedata(struct d2d_draw_seq* ds, double x, double y, double width, double height, void* buffer, unsigned long buffer_len) { +void d2d_getimagedata(struct d2d_draw_seq* ds, long id, double x, double y, double width, double height) { struct d2dins_getimagedata* r = twr_cache_malloc(sizeof(struct d2dins_getimagedata)); r->hdr.type = D2D_GETIMAGEDATA; r->x = x; r->y = y; r->width = width; r->height = height; - r->buffer = buffer; - r->buffer_len = buffer_len; - set_ptrs(ds, &r->hdr, NULL); - d2d_flush(ds); + r->id = id; + set_ptrs(ds, &r->hdr, NULL, NULL); } unsigned long d2d_getimagedatasize(double width, double height) { @@ -621,5 +648,50 @@ unsigned long d2d_getimagedatasize(double width, double height) { const double bytes_per_pixel = colors_per_pixel * bytes_per_color; double pixels = width * height; double bytes = pixels * bytes_per_pixel; - return (unsigned long)twrCeil(bytes); + return (unsigned long)ceil(bytes); +} + +void d2d_imagedatatoc(struct d2d_draw_seq* ds, long id, void* buffer, unsigned long buffer_len) { + struct d2dins_imagedatatoc* r = twr_cache_malloc(sizeof(struct d2dins_imagedatatoc)); + r->hdr.type = D2D_IMAGEDATATOC; + r->id = id; + r->buffer = buffer; + r->buffer_len = buffer_len; + set_ptrs(ds, &r->hdr, NULL, NULL); + d2d_flush(ds); +} + +double d2d_getcanvaspropdouble(struct d2d_draw_seq* ds, const char* prop_name) { + struct d2dins_getcanvaspropdouble* r = twr_cache_malloc(sizeof(struct d2dins_getcanvaspropdouble)); + r->hdr.type = D2D_GETCANVASPROPDOUBLE; + r->prop_name = prop_name; + double ret_val; + r->val = &ret_val; + set_ptrs(ds, &r->hdr, NULL, NULL); + d2d_flush(ds); + return ret_val; +} +void d2d_getcanvaspropstring(struct d2d_draw_seq* ds, const char* prop_name, char* buffer, unsigned long buffer_len) { + struct d2dins_getcanvaspropstring* r = twr_cache_malloc(sizeof(struct d2dins_getcanvaspropstring)); + r->hdr.type = D2D_GETCANVASPROPSTRING; + r->prop_name = prop_name; + r->val = buffer; + r->max_len = buffer_len; + set_ptrs(ds, &r->hdr, NULL, NULL); + d2d_flush(ds); +} +void d2d_setcanvaspropdouble(struct d2d_draw_seq* ds, const char* prop_name, double val) { + struct d2dins_setcanvaspropdouble* r = twr_cache_malloc(sizeof(struct d2dins_setcanvaspropdouble)); + r->hdr.type = D2D_SETCANVASPROPDOUBLE; + r->prop_name = cache_strdup(prop_name); + r->val = val; + set_ptrs(ds, &r->hdr, (void*)r->prop_name, NULL); +} +void d2d_setcanvaspropstring(struct d2d_draw_seq* ds, const char* prop_name, const char* val) { + struct d2dins_setcanvaspropstring* r = twr_cache_malloc(sizeof(struct d2dins_setcanvaspropstring)); + r->hdr.type = D2D_SETCANVASPROPSTRING; + r->prop_name = cache_strdup(prop_name); + r->val = cache_strdup(val); + + set_ptrs(ds, &r->hdr, (void*)r->prop_name, (void*)r->val); } \ No newline at end of file diff --git a/source/twr-c/io.c b/source/twr-c/io.c index 263a3023..428f7a97 100644 --- a/source/twr-c/io.c +++ b/source/twr-c/io.c @@ -4,7 +4,7 @@ #include #include #include -#include +#include // twrUnicodeCodePointToCodePage, twrCodePageToUnicodeCodePoint #include "twr-io.h" #include "twr-crt.h" //twr_vcbprintf diff --git a/source/twr-c/iojscon.c b/source/twr-c/iojscon.c index d1fa2e43..02fc79bf 100644 --- a/source/twr-c/iojscon.c +++ b/source/twr-c/iojscon.c @@ -40,7 +40,7 @@ static int getc32(twr_ioconsole_t* io) static void setfocus(twr_ioconsole_t* io) { - twrSetFocus(((struct IoJSCon*)io)->jsid); + twrConSetFocus(((struct IoJSCon*)io)->jsid); } static void jsconclose(twr_ioconsole_t* io) @@ -146,7 +146,7 @@ twr_ioconsole_t* twr_jscon_singleton(int jsid) twr_ioconsole_t* twr_get_console(const char* name) { - const int id=twrGetConIDFromName(name); + const int id=twrConGetIDFromName(name); if (id<0) return NULL; diff --git a/source/twr-c/twr-audio.h b/source/twr-c/twr-audio.h new file mode 100644 index 00000000..8e0036a9 --- /dev/null +++ b/source/twr-c/twr-audio.h @@ -0,0 +1,71 @@ +#ifndef __TWR_AUDIO_H__ +#define __TWR_AUDIO_H__ + +#ifdef __cplusplus +extern "C" { +#endif + + +__attribute__((import_name("twrAudioFromFloatPCM"))) long twr_audio_from_float_pcm(long num_channels, long sample_rate, float* data, long singleChannelDataLen); +__attribute__((import_name("twrAudioFrom8bitPCM"))) long twr_audio_from_8bit_pcm(long number_channels, long sample_rate, char* data, long singleChannelDataLen); +__attribute__((import_name("twrAudioFrom16bitPCM"))) long twr_audio_from_16bit_pcm(long number_channels, long sample_rate, short* data, long singleChannelDataLen); +__attribute__((import_name("twrAudioFrom32bitPCM"))) long twr_audio_from_32bit_pcm(long number_channels, long sample_rate, int* data, long singleChannelDataLen); + +__attribute__((import_name("twrAudioGetFloatPCM"))) float* twr_audio_get_float_pcm(long node_id, long* singleChannelDataLenPtr, long* numChannelsPtr); +__attribute__((import_name("twrAudioGet8bitPCM"))) char* twr_audio_get_8bit_pcm(long node_id, long* singleChannelDataLenPtr, long* numChannelsPtr); +__attribute__((import_name("twrAudioGet16bitPCM"))) short* twr_audio_get_16bit_pcm(long node_id, long* singleChannelDataLenPtr, long* numChannelsPtr); +__attribute__((import_name("twrAudioGet32bitPCM"))) int* twr_audio_get_32bit_pcm(long node_id, long* singleChannelDataLenPtr, long* numChannelsPtr); + +__attribute__((import_name("twrAudioPlay"))) long twr_audio_play(long node_id); +__attribute__((import_name("twrAudioPlay"))) long twr_audio_play_volume(long node_id, double volume, double pan); +__attribute__((import_name("twrAudioPlay"))) long twr_audio_play_callback(long node_id, double volume, double pan, int finish_callback); + +struct PlayRangeFields { + double pan, volume; + int loop, finish_callback; + long sample_rate; +}; +struct PlayRangeFields twr_audio_default_play_range(); +__attribute__((import_name("twrAudioPlayRange"))) long twr_audio_play_range(long node_id, long start_sample, long end_sample); +long twr_audio_play_range_ex(long node_id, long start_sample, long end_sample, struct PlayRangeFields* fields); + +__attribute__((import_name("twrAudioPlaySync"))) long twr_audio_play_sync(long node_id); +__attribute__((import_name("twrAudioPlaySync"))) long twr_audio_play_sync_ex(long node_id, double volume, double pan); + + +struct PlayRangeSyncFields { + double pan, volume; + int loop; + long sample_rate; +}; +struct PlayRangeSyncFields twr_audio_default_play_range_sync(); +__attribute__((import_name("twrAudioPlayRangeSync"))) long twr_audio_play_range_sync(long node_id, long start_sample, long end_sample); +long twr_audio_play_range_sync_ex(long node_id, long start_sample, long end_sample, struct PlayRangeSyncFields* fields); + +__attribute__((import_name("twrAudioLoadSync"))) long twr_audio_load_sync(char* url); +__attribute__((import_name("twrAudioLoad"))) long twr_audio_load(int event_id, char* url); +__attribute__((import_name("twrAudioQueryPlaybackPosition"))) long twr_audio_query_playback_position(long playback_id); +__attribute__((import_name("twrAudioFreeID"))) void twr_audio_free_id(long node_id); + +__attribute__((import_name("twrAudioStopPlayback"))) void twr_audio_stop_playback(long playback_id); + +__attribute__((import_name("twrAudioModifyPlaybackVolume"))) void twr_audio_modify_playback_volume(long playback_id, double volume); +__attribute__((import_name("twrAudioModifyPlaybackPan"))) void twr_audio_modify_playback_pan(long playback_id, double pan); +__attribute__((import_name("twrAudioModifyPlaybackRate"))) void twr_audio_modify_playback_rate(long playback_id, double sample_rate); + +__attribute__((import_name("twrAudioPlayFile"))) long twr_audio_play_file(char* file_url); +__attribute__((import_name("twrAudioPlayFile"))) long twr_audio_play_file_ex(char* file_url, double volume, double playback_rate, int loop); +struct AudioMetadata { + long length; + long sample_rate; + long channels; +}; + +__attribute__((import_name("twrAudioGetMetadata"))) void twr_audio_get_metadata(long node_id, struct AudioMetadata* metadata); + + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/source/twr-c/twr-crt.h b/source/twr-c/twr-crt.h index ff2a2bcd..997dccd7 100644 --- a/source/twr-c/twr-crt.h +++ b/source/twr-c/twr-crt.h @@ -52,10 +52,17 @@ char* twr_mbgets(char* buffer); _Noreturn void twr_trap(void); -void twr_sleep(int ms); -uint64_t twr_epoch_timems(); -void twr_tofixed(char* buffer, int buffer_size, double value, int dec_digits); -void twr_toexponential(char* buffer, int buffer_size, double value, int dec_digits); +__attribute__((import_name("twr_sleep"))) void twr_sleep(int ms); +__attribute__((import_name("twrTimeEpoch"))) uint64_t twr_epoch_timems(void); +__attribute__((import_name("twr_timer_single_shot"))) int twr_timer_single_shot(int milliSeconds, int eventID); +__attribute__((import_name("twr_timer_repeat"))) int twr_timer_repeat(int milliSeconds, int eventID); +__attribute__((import_name("twr_timer_cancel"))) void twr_timer_cancel(int timerID); + +// does not use locale information; it always uses . (a dot) as the decimal separator. +__attribute__((import_name("twrToFixed"))) double twr_tofixed(char* buffer, int buffer_size, double value, int dec_digits); + +// does not use locale information; it always uses . (a dot) as the decimal separator. +__attribute__((import_name("twrToExponential"))) void twr_toexponential(char* buffer, int buffer_size, double value, int dec_digits); const char* twr_get_navlang(int *len); @@ -65,8 +72,11 @@ void twr_utf32_to_code_page(char*out, int utf32); int twr_code_page_to_utf32_streamed(unsigned char byte); void twr_localize_numeric_string(char* str, locale_t locale); +/* library functions */ +__attribute__((import_name("twr_register_callback"))) int twr_register_callback(const char* func_name); + /* internal utility function */ -void nstrcopy(char *buffer, const int sizeInBytes, const char *outstring, const int sizeofoutstring, int n); +void __nstrcopy(char *buffer, const int sizeInBytes, const char *outstring, const int sizeofoutstring, int n); /* unit tests */ int malloc_unit_test(void); @@ -74,7 +84,8 @@ int string_unit_test(void); int mbstring_unit_test(void); int char_unit_test(void); int rand_unit_test(void); -int stdlib_unit_test(void); +int math_unit_test(void); +int misc_unit_test(void); int cvtint_unit_test(void); int cvtfloat_unit_test(void); int fcvt_unit_test(void); diff --git a/source/twr-c/twr-draw2d.h b/source/twr-c/twr-draw2d.h index 7b024757..a2c9f2f6 100644 --- a/source/twr-c/twr-draw2d.h +++ b/source/twr-c/twr-draw2d.h @@ -60,6 +60,11 @@ enum D2D_Types { D2D_SETLINEJOIN = 56, D2D_SETLINEDASHOFFSET = 57, D2D_GETIMAGEDATA = 58, + D2D_IMAGEDATATOC = 59, + D2D_GETCANVASPROPDOUBLE = 60, + D2D_GETCANVASPROPSTRING = 61, + D2D_SETCANVASPROPDOUBLE = 62, + D2D_SETCANVASPROPSTRING = 63, }; #define RGB_TO_RGBA(x) ( ((x)<<8) | 0xFF) @@ -68,7 +73,7 @@ struct d2d_instruction_hdr { struct d2d_instruction_hdr *next; unsigned long type; void* heap_ptr; - long padding; + void* heap_ptr2; }; struct d2dins_fillrect { @@ -177,7 +182,7 @@ struct d2dins_bezierto { double x, y; }; -struct d2dins_image_data { +struct d2dins_c_to_image_data { struct d2d_instruction_hdr hdr; unsigned long start; unsigned long length; @@ -325,7 +330,10 @@ struct d2dins_getlinedashlength { struct d2dins_drawimage { struct d2d_instruction_hdr hdr; + double sx, sy; + double sWidth, sHeight; double dx, dy; + double dWidth, dHeight; long id; }; @@ -358,8 +366,38 @@ struct d2dins_getimagedata { struct d2d_instruction_hdr hdr; double x, y; double width, height; - void* buffer; - unsigned long buffer_len; + long id; +}; +struct d2dins_imagedatatoc { + struct d2d_instruction_hdr hdr; + void* buffer; + unsigned long buffer_len; + long id; +}; + +struct d2dins_getcanvaspropdouble { + struct d2d_instruction_hdr hdr; + double* val; + const char* prop_name; +}; + +struct d2dins_getcanvaspropstring { + struct d2d_instruction_hdr hdr; + char* val; + unsigned long max_len; + const char* prop_name; +}; + +struct d2dins_setcanvaspropdouble { + struct d2d_instruction_hdr hdr; + double val; + const char* prop_name; +}; + +struct d2dins_setcanvaspropstring { + struct d2d_instruction_hdr hdr; + const char* val; + const char* prop_name; }; struct d2d_draw_seq { @@ -389,6 +427,9 @@ struct d2d_2d_matrix { double a, b, c, d, e, f; }; +__attribute__((import_name("twrConDrawSeq"))) void twrConDrawSeq(int jsid, struct d2d_draw_seq *); +__attribute__((import_name("twrConLoadImage"))) bool twrConLoadImage(int jsid, const char* url, long id); + struct d2d_draw_seq* d2d_start_draw_sequence(int flush_at_ins_count); struct d2d_draw_seq* d2d_start_draw_sequence_with_con(int flush_at_ins_count, twr_ioconsole_t * con); void d2d_end_draw_sequence(struct d2d_draw_seq* ds); @@ -413,6 +454,9 @@ void d2d_setfillstyle(struct d2d_draw_seq* ds, const char* css_color); void d2d_setfont(struct d2d_draw_seq* ds, const char* font); void d2d_setlinecap(struct d2d_draw_seq* ds, const char* line_cap); void d2d_setlinejoin(struct d2d_draw_seq* ds, const char* line_join); +void d2d_setlinedash(struct d2d_draw_seq* ds, unsigned long len, const double* segments); +unsigned long d2d_getlinedash(struct d2d_draw_seq* ds, unsigned long length, double* buffer); +unsigned long d2d_getlinedashlength(struct d2d_draw_seq* ds); void d2d_setlinedashoffset(struct d2d_draw_seq* ds, double line_dash_offset); void d2d_createlineargradient(struct d2d_draw_seq* ds, long id, double x0, double y0, double x1, double y1); @@ -436,6 +480,7 @@ void d2d_rect(struct d2d_draw_seq* ds, double x, double y, double width, double void d2d_closepath(struct d2d_draw_seq* ds); void d2d_imagedata(struct d2d_draw_seq* ds, long id, void* mem, unsigned long length, unsigned long width, unsigned long height); +void d2d_ctoimagedata(struct d2d_draw_seq* ds, long id, void* mem, unsigned long length, unsigned long width, unsigned long height); void d2d_putimagedata(struct d2d_draw_seq* ds, long id, unsigned long dx, unsigned long dy); void d2d_putimagedatadirty(struct d2d_draw_seq* ds, long id, unsigned long dx, unsigned long dy, unsigned long dirtyX, unsigned long dirtyY, unsigned long dirtyWidth, unsigned long dirtyHeight); @@ -450,15 +495,20 @@ void d2d_settransformmatrix(struct d2d_draw_seq* ds, const struct d2d_2d_matrix void d2d_transform(struct d2d_draw_seq* ds, double a, double b, double c, double d, double e, double f); void d2d_transformmatrix(struct d2d_draw_seq* ds, const struct d2d_2d_matrix * transform); void d2d_resettransform(struct d2d_draw_seq* ds); -void d2d_setlinedash(struct d2d_draw_seq* ds, unsigned long len, const double* segments); -unsigned long d2d_getlinedash(struct d2d_draw_seq* ds, unsigned long length, double* buffer); -unsigned long d2d_getlinedashlength(struct d2d_draw_seq* ds); bool d2d_load_image(const char* url, long id); bool d2d_load_image_with_con(const char* url, long id, twr_ioconsole_t * con); void d2d_drawimage(struct d2d_draw_seq* ds, long id, double dx, double dy); -void d2d_getimagedata(struct d2d_draw_seq* ds, double x, double y, double width, double height, void* buffer, unsigned long buffer_len); +void d2d_drawimage_ex(struct d2d_draw_seq* ds, long id, double sx, double sy, double sWidth, double sHeight, double dx, double dy, double dWidth, double dHeight); +void d2d_getimagedata(struct d2d_draw_seq* ds, long id, double x, double y, double width, double height); unsigned long d2d_getimagedatasize(double width, double height); +void d2d_imagedatatoc(struct d2d_draw_seq* ds, long id, void* buffer, unsigned long buffer_len); + +double d2d_getcanvaspropdouble(struct d2d_draw_seq* ds, const char* prop_name); +void d2d_getcanvaspropstring(struct d2d_draw_seq* ds, const char* prop_name, char* buffer, unsigned long buffer_len); +void d2d_setcanvaspropdouble(struct d2d_draw_seq* ds, const char* prop_name, double val); +void d2d_setcanvaspropstring(struct d2d_draw_seq* ds, const char* prop_name, const char* val); + #ifdef __cplusplus } #endif diff --git a/source/twr-c/twr-io.h b/source/twr-c/twr-io.h index 7c1406d2..b87e7301 100644 --- a/source/twr-c/twr-io.h +++ b/source/twr-c/twr-io.h @@ -84,6 +84,20 @@ struct IoConsole { #define TRS80_GRAPHIC_MARKER_MASK 0xFF00 #define TRS80_GRAPHIC_CHAR_MASK 0x003F // would be 0xC0 if we included the graphics marker bit 0x80 +__attribute__((import_name("twrConCharOut"))) void twrConCharOut(int jsid, int c, int code_page); +__attribute__((import_name("twrConPutStr"))) void twrConPutStr(int jsid, const char * str, int code_page); +__attribute__((import_name("twrConCharIn"))) int twrConCharIn(int jsid); +__attribute__((import_name("twrConGetProp"))) int twrConGetProp(int jsid, const char* prop_name); +__attribute__((import_name("twrConCls"))) void twrConCls(int jsid); +__attribute__((import_name("twrConSetC32"))) void twrConSetC32(int jsid, int location, int c32); +__attribute__((import_name("twrConSetReset"))) void twrConSetReset(int jsid, int x, int y, bool isset); +__attribute__((import_name("twrConPoint"))) int twrConPoint(int jsid, int x, int y); +__attribute__((import_name("twrConSetCursor"))) void twrConSetCursor(int jsid, int position); +__attribute__((import_name("twrConSetColors"))) void twrConSetColors(int jsid, unsigned long foreground, unsigned long background); +__attribute__((import_name("twrConSetRange"))) void twrConSetRange(int jsid, int * chars, int start, int len); +__attribute__((import_name("twrConSetFocus"))) void twrConSetFocus(int jsid); +__attribute__((import_name("twrConGetIDFromName"))) int twrConGetIDFromName(const char* name); + /* ionull.c */ twr_ioconsole_t* io_nullcon(void); diff --git a/source/twr-c/twr-jsimports.h b/source/twr-c/twr-jsimports.h index 98c1569b..6f7f45e9 100644 --- a/source/twr-c/twr-jsimports.h +++ b/source/twr-c/twr-jsimports.h @@ -2,7 +2,6 @@ #define __TWR_JSIMPORTS_H__ #include -#include "twr-draw2d.h" #include /* WebAssembly.ModuleImports (Javascript/TypeScript functions callable by C code) */ @@ -13,25 +12,6 @@ extern "C" { #endif -__attribute__((import_name("twrConCharOut"))) void twrConCharOut(int jsid, int c, int code_page); -__attribute__((import_name("twrConPutStr"))) void twrConPutStr(int jsid, const char * str, int code_page); -__attribute__((import_name("twrConCharIn"))) int twrConCharIn(int jsid); -__attribute__((import_name("twrConGetProp"))) int twrConGetProp(int jsid, const char* prop_name); -__attribute__((import_name("twrConCls"))) void twrConCls(int jsid); -__attribute__((import_name("twrConSetC32"))) void twrConSetC32(int jsid, int location, int c32); -__attribute__((import_name("twrConSetReset"))) void twrConSetReset(int jsid, int x, int y, bool isset); -__attribute__((import_name("twrConPoint"))) int twrConPoint(int jsid, int x, int y); -__attribute__((import_name("twrConSetCursor"))) void twrConSetCursor(int jsid, int position); -__attribute__((import_name("twrConSetColors"))) void twrConSetColors(int jsid, unsigned long foreground, unsigned long background); -__attribute__((import_name("twrConSetRange"))) void twrConSetRange(int jsid, int * chars, int start, int len); -__attribute__((import_name("twrConDrawSeq"))) void twrConDrawSeq(int jsid, struct d2d_draw_seq *); -__attribute__((import_name("twrConLoadImage"))) bool twrConLoadImage(int jsid, const char* url, long id); -__attribute__((import_name("twrSetFocus"))) void twrSetFocus(int jsid); -__attribute__((import_name("twrGetConIDFromName"))) int twrGetConIDFromName(const char* name); - - -__attribute__((import_name("twrSleep"))) void twrSleep(int ms); -__attribute__((import_name("twrTimeEpoch"))) double twrTimeEpoch(); __attribute__((import_name("twrTimeTmLocal"))) void twrTimeTmLocal(struct tm*, const time_t); __attribute__((import_name("twrUserLconv"))) void twrUserLconv(struct lconv *, int code_page); __attribute__((import_name("twrUserLanguage"))) char* twrUserLanguage(void); @@ -47,6 +27,7 @@ __attribute__((import_name("twrFAbs"))) double twrFAbs(double arg); __attribute__((import_name("twrACos"))) double twrACos(double arg); __attribute__((import_name("twrASin"))) double twrASin(double arg); __attribute__((import_name("twrATan"))) double twrATan(double arg); +__attribute__((import_name("twrATan2"))) double twrATan2(double y, double x); __attribute__((import_name("twrCos"))) double twrCos(double rad); __attribute__((import_name("twrSin"))) double twrSin(double rad); __attribute__((import_name("twrTan"))) double twrTan(double rad); @@ -60,18 +41,10 @@ __attribute__((import_name("twrSqrt"))) double twrSqrt(double arg); __attribute__((import_name("twrTrunc"))) double twrTrunc(double arg); __attribute__((import_name("twrAtod"))) double twrAtod(const char* str, int len); -__attribute__((import_name("twrToFixed"))) double twrToFixed(char* buffer, int buffer_size, double value, int dec_digits); -__attribute__((import_name("twrToExponential"))) void twrToExponential(char* buffer, int buffer_size, double value, int dec_digits); -__attribute__((import_name("twrDtoa"))) void twrDtoa(char* buffer, int buffer_size, double value, int max_precision); -__attribute__((import_name("twrFcvtS"))) int twrFcvtS( - char* buffer, - unsigned long sizeInBytes, //size_t - double value, - int fracpart_numdigits, - int *dec, - int *sign -); +// does not use locale information; it always uses . (a dot) as the decimal separator. +// twr_localize_numeric_string() is available to convert buffer if needed +__attribute__((import_name("twrDtoa"))) void twr_dtoa(char* buffer, int buffer_size, double value, int max_precision); #ifdef __cplusplus } diff --git a/source/twr-c/waitingcalls.c b/source/twr-c/waitingcalls.c deleted file mode 100644 index 34abe785..00000000 --- a/source/twr-c/waitingcalls.c +++ /dev/null @@ -1,15 +0,0 @@ - -#include "twr-crt.h" -#include "twr-jsimports.h" - -// Matches TS class twrWaitingCalls - -void twr_sleep(int ms) { - twrSleep(ms); // blocking call implemented by twrWasModuleAsync -} - -// ms since epoch -uint64_t twr_epoch_timems() { - return (uint64_t)twrTimeEpoch(); -} - diff --git a/source/twr-gcc-unit-test/main.c b/source/twr-gcc-unit-test/main.c index a37e20aa..56ca185e 100644 --- a/source/twr-gcc-unit-test/main.c +++ b/source/twr-gcc-unit-test/main.c @@ -39,7 +39,7 @@ int main() { printf("string unit test failed\n"); if (twr_rand_unit_test()==0) printf("rand unit test failed\n"); - if (stdlib_unit_test()==0) + if (misc_unit_test()==0) printf("misc unit test failed\n"); if (twr_big_run_unit_tests()==0) printf("bigint unit test failed\n"); diff --git a/source/twr-stdclib/cvtfloat.c b/source/twr-stdclib/cvtfloat.c index 3ed43eb0..fa690cdb 100644 --- a/source/twr-stdclib/cvtfloat.c +++ b/source/twr-stdclib/cvtfloat.c @@ -5,38 +5,6 @@ #include "twr-crt.h" #include "twr-jsimports.h" -// does not use locale information; it always uses . (a dot) as the decimal separator. -// twr_localize_numeric_string() is available to convert buffer if needed -void twr_dtoa(char* buffer, int buffer_size, double value, int max_precision) { - twrDtoa(buffer, buffer_size, value, max_precision); -} - -// does not use locale information; it always uses . (a dot) as the decimal separator. -void twr_tofixed(char* buffer, int buffer_size, double value, int dec_digits) { - twrToFixed(buffer, buffer_size, value, dec_digits); -} - -// does not use locale information; it always uses . (a dot) as the decimal separator. -void twr_toexponential(char* buffer, int buffer_size, double value, int dec_digits) { - twrToExponential(buffer, buffer_size, value, dec_digits); -} - -//The fcvt_s() function in C does not use locale information; it always uses . (a dot) as the decimal separator. -int _fcvt_s( - char* buffer, - unsigned long sizeInBytes, //size_t - double value, - int fracpart_numdigits, - int *dec, - int *sign -) { - return twrFcvtS(buffer, sizeInBytes, value, fracpart_numdigits, dec, sign); -} - -/****************************************************************/ -/****************************************************************/ -/****************************************************************/ - // [whitespace] [sign] [digits] [.digits] [ {e | E }[sign]digits] // also allow (fortran style) d/D instead of e/E // use locale-specific decimal separators. diff --git a/source/twr-stdclib/float/dtoa.c b/source/twr-stdclib/float/dtoa.c index 7aac3b8b..13bb2d6d 100644 --- a/source/twr-stdclib/float/dtoa.c +++ b/source/twr-stdclib/float/dtoa.c @@ -145,14 +145,14 @@ void twr_dtoa(char* buffer, int buffer_size, double value, int max_precision) { here=here+intlen; herelen=herelen+intlen; assert(buffer_size>herelen); const char zstr[]="00000000000000000000"; // 20 zeros - nstrcopy(here, buffer_size-herelen, zstr, sizeof(zstr), dec-n); + __nstrcopy(here, buffer_size-herelen, zstr, sizeof(zstr), dec-n); } else if (dec <=0 && dec > -5) { // fraction with up to 4 leading zeros strcpy(here, "0."); here+=2;herelen+=2; const char zstr[]="00000000000000000000"; // 20 zeros const int zlen=-dec; - nstrcopy(here, buffer_size-herelen, zstr, sizeof(zstr), zlen); + __nstrcopy(here, buffer_size-herelen, zstr, sizeof(zstr), zlen); here=here+zlen; herelen=herelen+zlen; assert(buffer_size>herelen); twr_big_itoa(&trial.num, here, buffer_size-herelen, 10); diff --git a/source/twr-stdclib/float/fcvt.c b/source/twr-stdclib/float/fcvt.c index 2d607b52..eeabccd6 100644 --- a/source/twr-stdclib/float/fcvt.c +++ b/source/twr-stdclib/float/fcvt.c @@ -30,19 +30,19 @@ int _fcvt_s( if (fieval.isinf) { /* infinity */ char infstr[]="1#INF00000000000000000000000000000"; - nstrcopy(buffer, sizeInBytes, infstr, sizeof(infstr), fracpart_numdigits+1); + __nstrcopy(buffer, sizeInBytes, infstr, sizeof(infstr), fracpart_numdigits+1); *dec=1; return 0; } else if (fieval.isnan) { /* nan */ char nanstr[]="1#QNAN00000000000000000000000000000"; - nstrcopy(buffer, sizeInBytes, nanstr, sizeof(nanstr), fracpart_numdigits+1); + __nstrcopy(buffer, sizeInBytes, nanstr, sizeof(nanstr), fracpart_numdigits+1); *dec=1; return 0; } else if (fieval.iszero) { /* zero */ char zeros[] ="000000000000000000000000000000000000"; - nstrcopy(buffer, sizeInBytes, zeros, sizeof(zeros), fracpart_numdigits); + __nstrcopy(buffer, sizeInBytes, zeros, sizeof(zeros), fracpart_numdigits); *dec=0; return 0; } diff --git a/source/twr-stdclib/include/math.h b/source/twr-stdclib/include/math.h index 862ba6c0..9901d1b3 100644 --- a/source/twr-stdclib/include/math.h +++ b/source/twr-stdclib/include/math.h @@ -5,10 +5,10 @@ extern "C" { #endif -int abs(int n); double acos(double arg); double asin(double arg); double atan(double arg); +double atan2(double y, double x); double ceil(double arg); double cos(double arg); double exp(double arg); diff --git a/source/twr-stdclib/include/stdlib.h b/source/twr-stdclib/include/stdlib.h index b9f2f9ca..5b0aa52d 100644 --- a/source/twr-stdclib/include/stdlib.h +++ b/source/twr-stdclib/include/stdlib.h @@ -16,7 +16,6 @@ extern "C" { /************************/ - void *malloc(size_t size); void free(void *mem); size_t avail(void); @@ -35,16 +34,20 @@ void srand(int seed); #define __min(a,b) (((a) < (b)) ? (a) : (b)) #define __max(a,b) (((a) > (b)) ? (a) : (b)) +int abs(int n); + /************************/ -int _fcvt_s( +//The fcvt_s() function in C does not use locale information; it always uses . (a dot) as the decimal separator. +__attribute__((import_name("twrFcvtS"))) int _fcvt_s( char* buffer, - size_t sizeInBytes, + unsigned long sizeInBytes, //size_t double value, int fracpart_numdigits, int *dec, int *sign ); + double atof(const char* str); int atoi(const char *str); long atol( const char *str ); diff --git a/source/twr-stdclib/math.c b/source/twr-stdclib/math.c index faaada0a..cc32de1b 100644 --- a/source/twr-stdclib/math.c +++ b/source/twr-stdclib/math.c @@ -1,10 +1,10 @@ #include #include "twr-jsimports.h" -int abs(int n) { - if (n<0) return -n; - else return n; -} +// It is not possible to define the math library imports like this: +// __attribute__((import_name("twrASin"))) double sin(double arg); +// as the compiler optimizes math functions like sin, and even at -O0, ignores the custom math.h +// using the clang flag -fno-builtin solves this issue, but doesn't seem like an optimizing solution double fabs (double arg) { return twrFAbs(arg); @@ -22,6 +22,10 @@ double atan(double arg) { return twrATan(arg); } +double atan2(double y, double x) { + return twrATan2(y, x); +} + double cos(double rad) { return twrCos(rad); } @@ -66,3 +70,44 @@ double trunc(double arg) { return twrTrunc(arg); } +// math smoke tests. Not comprehensive, because these call JavaScript which presumably has its own tests +int math_unit_test() { + + if (sqrt(4)!=2) return 0; + if (sqrt(6.25)!=2.5) return 0; + + if (trunc(5.7) !=5.000) return 0; + if (trunc(-5.7) !=-5.000) return 0; + + if (ceil(5.7) !=6.000) return 0; + if (ceil(-5.7) !=-5.000) return 0; + + if (floor(5.7) !=5.000) return 0; + if (floor(-5.7) !=-6.000) return 0; + + if (pow(2.0, 3.0)!=8.0) return 0; + + if (log(1)!=0) return 0; + + if (fmod(10.0, 3.0)!=1.0) return 0; + + if (exp(0)!=1.0) return 0; + + if (tan(0)!=0) return 0; + + if (sin(0)!=0) return 0; + + if (cos(0)!=1) return 0; + + if (fabs(1.0)!=1.0) return 0; + if (fabs(-1.0)!=1.0) return 0; + + if (atan(0)!=0) return 0; + + if (asin(0)!=0) return 0; + + if (acos(1)!=0) return 0; + + return 1; +} + diff --git a/source/twr-stdclib/stdlib.c b/source/twr-stdclib/misc.c similarity index 58% rename from source/twr-stdclib/stdlib.c rename to source/twr-stdclib/misc.c index 4f35d2ee..f563019f 100644 --- a/source/twr-stdclib/stdlib.c +++ b/source/twr-stdclib/misc.c @@ -2,18 +2,15 @@ #include #include -int stdlib_unit_test() { - if (__min(5, 100)!=5) return 0; - if (__max(5, 100)!=100) return 0; - - return 1; +int abs(int n) { + if (n<0) return -n; + else return n; } - /**************************************************/ // for internal use, not an export -void nstrcopy(char *dest, const int sizeInBytes, const char *src, const int sizeofsrc, int n) { +void __nstrcopy(char *dest, const int sizeInBytes, const char *src, const int sizeofsrc, int n) { if (n>0) { if (n>sizeofsrc) n = sizeofsrc; if (n>sizeInBytes-1) n=sizeInBytes-1; @@ -23,6 +20,16 @@ void nstrcopy(char *dest, const int sizeInBytes, const char *src, const int size else if (sizeInBytes>0) dest[0]=0; } +/**************************************************/ + +int misc_unit_test() { + if (__min(5, 100)!=5) return 0; + if (__max(5, 100)!=100) return 0; + + if (abs(5)!=5) return 0; + if (abs(-5)!=5) return 0; + return 1; +} diff --git a/source/twr-stdclib/printf.c b/source/twr-stdclib/printf.c index 143f4b6f..b475f552 100644 --- a/source/twr-stdclib/printf.c +++ b/source/twr-stdclib/printf.c @@ -20,7 +20,7 @@ static void outstr(twr_vcbprintf_callback out, void* cbdata, char *buffer, int s } #define valid_flag(flag) (flag=='-' || flag==' ' || flag=='+' || flag=='#' || flag=='0') -#define valid_specifier(sp) (sp=='d' || sp=='x' || sp=='s' || sp=='f' || sp=='g' || sp=='e' || sp=='c') +#define valid_specifier(sp) (sp=='d' || sp=='u' || sp=='x' || sp=='s' || sp=='f' || sp=='g' || sp=='e' || sp=='c') //%[flags][width][.precision][length]specifier //valid lengths: h hh l ll j z t L @@ -92,7 +92,7 @@ void static do_width(const char* in, char* assembly, int size_assembly, bool pad const int len=strlen(in); int padlen=width-len; if (padlen<0) padlen=0; - nstrcopy(assembly, size_assembly, pad_zeros?zstr:spcstr, sizeof(zstr), padlen); + __nstrcopy(assembly, size_assembly, pad_zeros?zstr:spcstr, sizeof(zstr), padlen); strcat_s(assembly, size_assembly, in); } @@ -105,11 +105,14 @@ void twr_vcbprintf(twr_vcbprintf_callback out, void* cbdata, const char *format, format=read_format(format, &pf, &vlist); switch (pf.specifier) { case 'd': + case 'u': { char buffer[20]; char assembly[20]; int assemoff; - int val=va_arg(vlist, int); + int64_t val; + if (pf.specifier=='d') val=va_arg(vlist, int); + else val=va_arg(vlist, unsigned int); _itoa_s(val, buffer, sizeof(buffer), 10); if (val>=0 && pf.flag_space) { @@ -559,7 +562,13 @@ int printf_unit_test() { //snprintf(b, sizeof(b), "%6.2d", -5); // NOT IMPLEMENTED YET //twr_conlog("'%s'",b); //if (strcmp(b, " -05")!=0) return 0; - + +// u + snprintf(b, sizeof(b), "%u", 1); + if (strcmp(b, "1")!=0) return 0; + + snprintf(b, sizeof(b), "%u", -1); + if (strcmp(b, "4294967295")!=0) return 0; // c diff --git a/source/twr-ts/index.ts b/source/twr-ts/index.ts index 5abb125e..69714d34 100644 --- a/source/twr-ts/index.ts +++ b/source/twr-ts/index.ts @@ -1,16 +1,14 @@ -import {twrWasmModule} from "./twrmod.js"; -import {twrWasmModuleAsync} from "./twrmodasync.js"; -import {IModOpts} from "./twrmodbase.js" -import {twrConsoleDiv} from "./twrcondiv.js" -import {twrConsoleTerminal} from "./twrconterm.js" -import {twrConsoleDebug} from "./twrcondebug.js" -import {twrConsoleCanvas} from "./twrconcanvas.js" +export * from "./twrwasmbase.js" +export * from "./twrmod.js" +export * from "./twrwasmmem.js" +export * from "./twrmodasync.js" +export * from "./twrcondiv.js" +export * from "./twrconterm.js" +export * from "./twrcon.js" +export * from "./twrcondebug.js" +export * from "./twrconcanvas.js" +export * from "./twrlibrary.js" + + -export {IModOpts}; -export {twrWasmModule}; -export {twrWasmModuleAsync}; -export {twrConsoleDiv}; -export {twrConsoleTerminal}; -export {twrConsoleDebug}; -export {twrConsoleCanvas}; diff --git a/source/twr-ts/tsconfig.json b/source/twr-ts/tsconfig.json index 66cb8ead..1cc8d944 100644 --- a/source/twr-ts/tsconfig.json +++ b/source/twr-ts/tsconfig.json @@ -3,8 +3,9 @@ /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */ - "incremental": false, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - "composite": false, /* Enable constraints that allow a TypeScript project to be used with project references. */ + "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + /* Composite projects may not disable incremental compilation. */ + "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ /* Language and Environment */ "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ @@ -19,7 +20,7 @@ "declarationMap": true, /* Create sourcemaps for d.ts files. */ "sourceMap": true, /* Create source map files for emitted JavaScript files. */ "outDir": "../../lib-js", /* Specify an output folder for all emitted files. */ - + /* Interop Constraints */ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ diff --git a/source/twr-ts/twrcircular.ts b/source/twr-ts/twrcircular.ts index 63885601..398a42e1 100644 --- a/source/twr-ts/twrcircular.ts +++ b/source/twr-ts/twrcircular.ts @@ -6,8 +6,8 @@ // readWait() is used used when io_getc32() or io_mbgetstr() is called from a C function. // -const RDIDX=256; -const WRIDX=257; +const RDIDX=0; +const WRIDX=1; const LEN=256; // A single thread can read and a separate single thread can write. With these constraints Atomic operations are not needed. @@ -17,51 +17,70 @@ const LEN=256; //!!!! I am using --enable-features=SharedArrayBuffer; see the SharedArrayBuffer docs for COR issues when going to a live web server export class twrSharedCircularBuffer { - sharedArray:SharedArrayBuffer; - buf:Int32Array; + saBuffer:SharedArrayBuffer; + f64Array:Float64Array; + i32Array:Int32Array; - constructor (sa?:SharedArrayBuffer) { - if (typeof window !== 'undefined') { // this check only works if window defined (not a worker thread) - if (!crossOriginIsolated && !(window.location.protocol === 'file:')) throw new Error("twrSharedCircularBuffer constructor, crossOriginIsolated="+crossOriginIsolated+". See SharedArrayBuffer docs."); - } - if (sa) this.sharedArray=sa; - else this.sharedArray=new SharedArrayBuffer(258*4); - this.buf=new Int32Array(this.sharedArray); - this.buf[RDIDX]=0; - this.buf[WRIDX]=0; - } + constructor (sa?:SharedArrayBuffer) { + if (typeof window !== 'undefined') { // this check only works if window defined (not a worker thread) + if (!crossOriginIsolated && !(window.location.protocol === 'file:')) + throw new Error("twrSharedCircularBuffer constructor, crossOriginIsolated="+crossOriginIsolated+". See SharedArrayBuffer docs."); + } + if (sa) this.saBuffer=sa; + else this.saBuffer=new SharedArrayBuffer(LEN*8+4+4); // LEN Float64's + RDIDX and WRIDX (both Int32) + this.f64Array=new Float64Array(this.saBuffer, 8); + this.i32Array=new Int32Array(this.saBuffer, 0, 2); + this.i32Array[RDIDX]=0; + this.i32Array[WRIDX]=0; + } + + private silentWrite(n:number) { + let i=this.i32Array[WRIDX]; + this.f64Array[i]=n; + i++; + if (i==LEN) i=0; + this.i32Array[WRIDX]=i; + } + + writeArray(arr:number[]) { + if (arr.length>0) { + for (let i=0; inumber; - getProxyParams: ()=> TConsoleProxyParams; - processMessage(msgType:string, data:[number, ...any[]], callingModule:twrWasmModuleBase):boolean; + twrConGetProp: (callingMod:IWasmModule|IWasmModuleAsync, pn:number)=>number; - id:number; // returned by twrConsoleRegistry.registerConsole() + id:number; element?:HTMLElement; // debug console does not have an element } -export interface IConsoleBaseProxy { - getProp: (propName: string)=>number; - id:number; // returned by twrConsoleRegistry.registerConsole() -} - -export interface IConsoleStream { - charOut: (c:number, codePage:number)=>void; +export interface IConsoleStreamOut { + twrConCharOut: (callingMod:IWasmModule|IWasmModuleAsync, c:number, codePage:number)=>void; + twrConPutStr: (callingMod:IWasmModule|IWasmModuleAsync, chars:number, codePage:number)=>void; + charOut: (ch:string)=>void; putStr: (str:string)=>void; - keyDown: (ev:KeyboardEvent)=>void; - - keys?: twrSharedCircularBuffer; // only created if getProxyParams is called } -export interface IConsoleStreamProxy { - charOut: (c:number, codePage:number)=>void; - putStr: (str:string)=>void; - charIn: ()=>number; - setFocus: ()=>void; +export interface IConsoleStreamIn { + twrConCharIn_async: (callingMod:IWasmModuleAsync)=>Promise; + twrConSetFocus: (callingMod:IWasmModuleAsync)=>void; + + //this should be called by JSMain thread to inject key events + keyDown: (ev:KeyboardEvent)=>void; } export interface IConsoleAddressable { - cls: ()=>void; - setRange: (start:number, values:[])=>void; - setC32: (location:number, char:number)=>void; - setReset: (x:number, y:number, isset:boolean)=>void; - point: (x:number, y:number)=>boolean; - setCursor: (pos:number)=>void; - setCursorXY: (x:number, y:number)=>void; - setColors: (foreground:number, background:number)=>void; + twrConCls: (callingMod:IWasmModule|IWasmModuleAsync)=>void; + setRangeJS: (start:number, values:[])=>void; + twrConSetRange: (callingMod:IWasmModule|IWasmModuleAsync, chars:number, start:number, len:number)=>void; + twrConSetC32: (callingMod:IWasmModule|IWasmModuleAsync, location:number, char:number)=>void; + twrConSetReset: (callingMod:IWasmModule|IWasmModuleAsync,x:number, y:number, isset:boolean)=>void; + twrConPoint: (callingMod:IWasmModule|IWasmModuleAsync, x:number, y:number)=>boolean; + twrConSetCursor: (callingMod:IWasmModule|IWasmModuleAsync, pos:number)=>void; + twrConSetCursorXY: (callingMod:IWasmModule|IWasmModuleAsync, x:number, y:number)=>void; + twrConSetColors: (callingMod:IWasmModule|IWasmModuleAsync, foreground:number, background:number)=>void; } export interface IConsoleDrawable { - drawSeq: (ds:number, owner:twrWasmModuleBase)=>void, - } - - export interface IConsoleDrawableProxy { - drawSeq: (ds:number)=>void, - loadImage: (urlPtr: number, id: number)=>number, -} - -export interface IConsoleTerminal extends IConsoleBase, IConsoleStream, IConsoleAddressable {} -export interface IConsoleTerminalProxy extends IConsoleBaseProxy, IConsoleStreamProxy, IConsoleAddressable {} - -export interface IConsoleDiv extends IConsoleBase, IConsoleStream {} -export interface IConsoleDivProxy extends IConsoleBaseProxy, IConsoleStreamProxy {} - -export interface IConsoleDebug extends IConsoleBase, IConsoleStream {} -export interface IConsoleDebugProxy extends IConsoleBaseProxy, IConsoleStreamProxy {} + twrConDrawSeq: (mod:IWasmModuleAsync|IWasmModule, ds:number)=>void, + twrConLoadImage_async: (mod:IWasmModuleAsync, urlPtr: number, id: number)=>Promise, + } +export interface IConsoleTerminal extends IConsoleBase, IConsoleStreamOut, IConsoleStreamIn, IConsoleAddressable {} +export interface IConsoleDiv extends IConsoleBase, IConsoleStreamOut, IConsoleStreamIn {} +export interface IConsoleDebug extends IConsoleBase, IConsoleStreamOut {} export interface IConsoleCanvas extends IConsoleBase, IConsoleDrawable {} -export interface IConsoleCanvasProxy extends IConsoleBaseProxy, IConsoleDrawableProxy {} - -export interface IConsole extends IConsoleBase, Partial, Partial, Partial {} -export interface IConsoleProxy extends IConsoleBaseProxy, Partial, Partial, Partial {} +export interface IConsole extends IConsoleBase, Partial, Partial, Partial, Partial {} -// ProxyParams are the info needed to instantiate the proxy version of a console -export type TConsoleDebugProxyParams = ["twrConsoleDebugProxy", number]; -export type TConsoleDivProxyParams = ["twrConsoleDivProxy", number, SharedArrayBuffer]; -export type TConsoleTerminalProxyParams = ["twrConsoleTerminalProxy", number, SharedArrayBuffer, SharedArrayBuffer]; -export type TConsoleCanvasProxyParams = ["twrConsoleCanvasProxy", number, ICanvasProps, SharedArrayBuffer, SharedArrayBuffer, SharedArrayBuffer]; -export type TConsoleProxyParams = TConsoleTerminalProxyParams | TConsoleDivProxyParams | TConsoleDebugProxyParams | TConsoleCanvasProxyParams; // must match IO_TYPEs in twr_io.h export class IOTypes { @@ -144,7 +120,7 @@ export class IOTypes { /////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////// -function keyEventProcess(ev:KeyboardEvent) { +export function keyEventToCodePoint(ev:KeyboardEvent) { if ( !ev.isComposing && !ev.metaKey && ev.key!="Control" && ev.key!="Alt" ) { //console.log("keyDownDiv: ",ev.key, ev.code, ev.key.codePointAt(0), ev); if (ev.key.length==1) @@ -170,14 +146,11 @@ function keyEventProcess(ev:KeyboardEvent) { return undefined; } -// this is a utility function used by console classes, -// and should be called from HTML "keydown" event -export function keyDownUtil(destinationCon:IConsole, ev:KeyboardEvent) { - if (!destinationCon.keys) - throw new Error("keyDown requires twrModuleAsync"); - else { - const r=keyEventProcess(ev); - if (r) destinationCon.keys.write(r); - } +export function logToCon(con:IConsole, ...params: string[]) { + for (var i = 0; i < params.length; i++) { + con.putStr!(params[i].toString()); + con.charOut!(' '); // space + } + con.charOut!('\n'); } diff --git a/source/twr-ts/twrconcanvas.ts b/source/twr-ts/twrconcanvas.ts index 8817e3c7..ef0151ec 100644 --- a/source/twr-ts/twrconcanvas.ts +++ b/source/twr-ts/twrconcanvas.ts @@ -1,8 +1,7 @@ -import {twrWasmModuleBase} from "./twrmodbase.js" -import {twrSharedCircularBuffer} from "./twrcircular.js"; -import {twrSignal} from "./twrsignal.js"; -import {IConsoleCanvas, IConsoleCanvasProxy, ICanvasProps, TConsoleCanvasProxyParams, IOTypes} from "./twrcon.js"; -import {twrConsoleRegistry} from "./twrconreg.js" +import {IConsoleCanvas, ICanvasProps, IOTypes} from "./twrcon.js"; +import {IWasmModuleAsync} from "./twrmodasync.js" +import {IWasmModule} from "./twrmod.js"; +import {twrLibrary, TLibImports, twrLibraryInstanceRegistry} from "./twrlibrary.js"; enum D2DType { D2D_FILLRECT=1, @@ -56,17 +55,19 @@ enum D2DType { D2D_SETLINEJOIN = 56, D2D_SETLINEDASHOFFSET = 57, D2D_GETIMAGEDATA = 58, + D2D_IMAGEDATATOC = 59, + D2D_GETCANVASPROPDOUBLE = 60, + D2D_GETCANVASPROPSTRING = 61, + D2D_SETCANVASPROPDOUBLE = 62, + D2D_SETCANVASPROPSTRING = 63, } -export class twrConsoleCanvas implements IConsoleCanvas { - ctx:CanvasRenderingContext2D; +export class twrConsoleCanvas extends twrLibrary implements IConsoleCanvas { id:number; + ctx:CanvasRenderingContext2D; element:HTMLCanvasElement props:ICanvasProps; - cmdCompleteSignal?:twrSignal; - canvasKeys?: twrSharedCircularBuffer; - returnValue?: twrSharedCircularBuffer; - isAsyncMod:boolean; + //TODO!! BUG - precomputed objects should be unique for each module that using this twrConsoleCanvas precomputedObjects: { [index: number]: (ImageData | {mem8:Uint8Array, width:number, height:number}) | @@ -74,8 +75,19 @@ export class twrConsoleCanvas implements IConsoleCanvas { HTMLImageElement }; + imports:TLibImports = { + twrConGetProp:{}, + twrConDrawSeq:{}, + twrConLoadImage:{isModuleAsyncOnly:true, isAsyncFunction:true}, + }; + + libSourcePath = new URL(import.meta.url).pathname; + interfaceName = "twrConsole"; + constructor(element:HTMLCanvasElement) { - this.isAsyncMod=false; // set to true if getProxyParams called + // all library constructors should start with these two lines + super(); + this.id=twrLibraryInstanceRegistry.register(this); this.precomputedObjects={}; @@ -93,725 +105,746 @@ export class twrConsoleCanvas implements IConsoleCanvas { c.textBaseline="top"; this.props = {canvasHeight: element.height, canvasWidth: element.width, type: IOTypes.CANVAS2D}; - this.id=twrConsoleRegistry.registerConsole(this); } - // these are the parameters needed to create a twrConsoleCanvasProxy, paired to us - getProxyParams() : TConsoleCanvasProxyParams { - this.cmdCompleteSignal=new twrSignal(); - this.canvasKeys = new twrSharedCircularBuffer(); // tsconfig, lib must be set to 2017 or higher - this.returnValue = new twrSharedCircularBuffer(); - this.isAsyncMod=true; - return ["twrConsoleCanvasProxy", this.id, this.props, this.cmdCompleteSignal.sharedArray, this.canvasKeys.sharedArray, this.returnValue.sharedArray]; - } - getProp(name:keyof ICanvasProps): number { + getProp(name:keyof ICanvasProps): number { return this.props[name]; } - // process messages sent from twrConsoleCanvasProxy - // these are used to "remote procedure call" from the worker thread to the JS Main thread - processMessage(msgType:string, data:[number, ...any[]], callingModule:twrWasmModuleBase):boolean { - const [id, ...params] = data; - if (id!=this.id) throw new Error("internal error"); // should never happen - - switch (msgType) { - case "canvas2d-drawseq": - { - const [ds] = params; - this.drawSeq(ds, callingModule); - break; + twrConGetProp(callingMod:IWasmModule|IWasmModuleAsync, pn:number):number { + const propName=callingMod.wasmMem.getString(pn); + return this.getProp(propName); + } + + twrConLoadImage_async(mod: IWasmModuleAsync, urlPtr: number, id: number) : Promise { + return new Promise( (resolve)=>{ + const url = mod.wasmMem.getString(urlPtr); + if ( id in this.precomputedObjects ) console.log("warning: D2D_LOADIMAGE ID already exists."); + + const img = new Image(); + img.onload = () => { + resolve(1); + }; + img.onerror = () => { + console.log("Warning: D2D_LOADIMAGE: failed to load image " + url); + resolve(1); } - case "canvas2d-loadimage": - { - const [urlPtr, id] = params; - this.loadImage(urlPtr, id, callingModule); - break; - } - default: - return false; - } + img.src = url; - return true; - } - - private loadImage(urlPtr: number, id: number, owner: twrWasmModuleBase) { - const url = owner.getString(urlPtr); - if ( id in this.precomputedObjects ) console.log("warning: D2D_LOADIMAGE ID already exists."); - - const img = new Image(); - const errorMsg = "`private loadImage` either has an internal error or is being misused"; - img.onload = () => { - if (this.returnValue) { - this.returnValue.write(1); - } - else throw new Error(errorMsg); - }; - img.onerror = () => { - if (this.returnValue) { - console.log("Warning: D2D_LOADIMAGE: failed to load image " + url); - this.returnValue.write(0); - } - else throw new Error(errorMsg); - } - - img.src = url; - - this.precomputedObjects[id] = img; + this.precomputedObjects[id] = img; + }); } /* see draw2d.h for structs that match */ - drawSeq(ds:number, owner:twrWasmModuleBase) { + twrConDrawSeq(mod:IWasmModuleAsync|IWasmModule, ds:number) { //console.log("twr::Canvas enter drawSeq"); if (!this.ctx) return; - const insHdrSize = 16; - let currentInsHdr=owner.getLong(ds); /* ds->start */ - const lastInsHdr=owner.getLong(ds+4); /* ds->last */ - let currentInsParams = currentInsHdr + insHdrSize; - //console.log("instruction start, last ",ins.toString(16), lastins.toString(16)); - - let nextInsHdr:number; - //let insCount=0; - - while (1) { - - //insCount++; - - const type:D2DType=owner.getLong(currentInsHdr+4); /* hdr->type */ - if (0/*type!=D2DType.D2D_FILLRECT*/) { - console.log("ins",currentInsHdr) - console.log("hdr.next",owner.mem8[currentInsHdr],owner.mem8[currentInsHdr+1],owner.mem8[currentInsHdr+2],owner.mem8[currentInsHdr+3]); - console.log("hdr.type",owner.mem8[currentInsHdr+4],owner.mem8[currentInsHdr+5]); - console.log("next 4 bytes", owner.mem8[currentInsHdr+6],owner.mem8[currentInsHdr+7],owner.mem8[currentInsHdr+8],owner.mem8[currentInsHdr+9]); - console.log("and 4 more ", owner.mem8[currentInsHdr+10],owner.mem8[currentInsHdr+11],owner.mem8[currentInsHdr+12],owner.mem8[currentInsHdr+13]); - //console.log("ins, type, next is ", ins.toString(16), type.toString(16), next.toString(16)); - } - switch (type) { - case D2DType.D2D_FILLRECT: - { - const x=owner.getDouble(currentInsParams); - const y=owner.getDouble(currentInsParams+8); - const w=owner.getDouble(currentInsParams+16); - const h=owner.getDouble(currentInsParams+24); - this.ctx.fillRect(x, y, w, h); - } - break; - - case D2DType.D2D_STROKERECT: - { - const x=owner.getDouble(currentInsParams); - const y=owner.getDouble(currentInsParams+8); - const w=owner.getDouble(currentInsParams+16); - const h=owner.getDouble(currentInsParams+24); - this.ctx.strokeRect(x, y, w, h); - } - break; - - case D2DType.D2D_FILLCODEPOINT: - { - const x=owner.getDouble(currentInsParams); - const y=owner.getDouble(currentInsParams+8); - const c=owner.getLong(currentInsParams+16); - let txt=String.fromCodePoint(c); - this.ctx.fillText(txt, x, y); - } - break; - - case D2DType.D2D_FILLTEXT: - { - const x=owner.getDouble(currentInsParams); - const y=owner.getDouble(currentInsParams+8); - const codePage=owner.getLong(currentInsParams+20); - const strPointer = owner.getLong(currentInsParams+16); - const str=owner.getString(strPointer, undefined, codePage); - - //console.log("filltext ",x,y,str) - - this.ctx.fillText(str, x, y); - } - break; + const wasmMem=mod.wasmMem; + const insHdrSize = 16; + let currentInsHdr=wasmMem.getLong(ds); /* ds->start */ + const lastInsHdr=wasmMem.getLong(ds+4); /* ds->last */ + let currentInsParams = currentInsHdr + insHdrSize; + //console.log("instruction start, last ",ins.toString(16), lastins.toString(16)); - case D2DType.D2D_MEASURETEXT: - { - const codePage=owner.getLong(currentInsParams+8); - const str=owner.getString(owner.getLong(currentInsParams), undefined, codePage); - const tmidx=owner.getLong(currentInsParams+4); - - const tm=this.ctx.measureText(str); - owner.setDouble(tmidx+0, tm.actualBoundingBoxAscent); - owner.setDouble(tmidx+8, tm.actualBoundingBoxDescent); - owner.setDouble(tmidx+16, tm.actualBoundingBoxLeft); - owner.setDouble(tmidx+24, tm.actualBoundingBoxRight); - owner.setDouble(tmidx+32, tm.fontBoundingBoxAscent); - owner.setDouble(tmidx+40, tm.fontBoundingBoxDescent); - owner.setDouble(tmidx+48, tm.width); - } - break; - - case D2DType.D2D_SETFONT: - { - const fontPointer = owner.getLong(currentInsParams); - const str=owner.getString(fontPointer); - this.ctx.font=str; - } - break; - - case D2DType.D2D_SETFILLSTYLERGBA: - { - const color=owner.getLong(currentInsParams); - const cssColor= "#"+("00000000" + color.toString(16)).slice(-8); - this.ctx.fillStyle = cssColor; - //console.log("fillstyle: ", this.ctx.fillStyle, ":", cssColor,":", color) - } - break; + let nextInsHdr:number; + //let insCount=0; + + while (1) { + + //insCount++; + + const type:D2DType=wasmMem.getLong(currentInsHdr+4); /* hdr->type */ + if (0/*type!=D2DType.D2D_FILLRECT*/) { + console.log("ins",currentInsHdr) + console.log("hdr.next",wasmMem.mem8[currentInsHdr],wasmMem.mem8[currentInsHdr+1],wasmMem.mem8[currentInsHdr+2],wasmMem.mem8[currentInsHdr+3]); + console.log("hdr.type",wasmMem.mem8[currentInsHdr+4],wasmMem.mem8[currentInsHdr+5]); + console.log("next 4 bytes", wasmMem.mem8[currentInsHdr+6],wasmMem.mem8[currentInsHdr+7],wasmMem.mem8[currentInsHdr+8],wasmMem.mem8[currentInsHdr+9]); + console.log("and 4 more ", wasmMem.mem8[currentInsHdr+10],wasmMem.mem8[currentInsHdr+11],wasmMem.mem8[currentInsHdr+12],wasmMem.mem8[currentInsHdr+13]); + //console.log("ins, type, next is ", ins.toString(16), type.toString(16), next.toString(16)); + } + switch (type) { + case D2DType.D2D_FILLRECT: + { + const x=wasmMem.getDouble(currentInsParams); + const y=wasmMem.getDouble(currentInsParams+8); + const w=wasmMem.getDouble(currentInsParams+16); + const h=wasmMem.getDouble(currentInsParams+24); + this.ctx.fillRect(x, y, w, h); + } + break; + + case D2DType.D2D_STROKERECT: + { + const x=wasmMem.getDouble(currentInsParams); + const y=wasmMem.getDouble(currentInsParams+8); + const w=wasmMem.getDouble(currentInsParams+16); + const h=wasmMem.getDouble(currentInsParams+24); + this.ctx.strokeRect(x, y, w, h); + } + break; + + case D2DType.D2D_FILLCODEPOINT: + { + const x=wasmMem.getDouble(currentInsParams); + const y=wasmMem.getDouble(currentInsParams+8); + const c=wasmMem.getLong(currentInsParams+16); + let txt=String.fromCodePoint(c); + this.ctx.fillText(txt, x, y); + } + break; - case D2DType.D2D_SETSTROKESTYLERGBA: - { - const color=owner.getLong(currentInsParams); - const cssColor= "#"+("00000000" + color.toString(16)).slice(-8); - this.ctx.strokeStyle = cssColor; - } - break; + + case D2DType.D2D_FILLTEXT: + { + const x=wasmMem.getDouble(currentInsParams); + const y=wasmMem.getDouble(currentInsParams+8); + const codePage=wasmMem.getLong(currentInsParams+20); + const strPointer = wasmMem.getLong(currentInsParams+16); + const str=wasmMem.getString(strPointer, undefined, codePage); - case D2DType.D2D_SETFILLSTYLE: - { - const cssColorPointer = owner.getLong(currentInsParams); - const cssColor= owner.getString(cssColorPointer); - this.ctx.fillStyle = cssColor; - } - break + //console.log("filltext ",x,y,str) - case D2DType.D2D_SETSTROKESTYLE: - { - const cssColorPointer = owner.getLong(currentInsParams); - const cssColor= owner.getString(cssColorPointer); - this.ctx.strokeStyle = cssColor; - } - break + this.ctx.fillText(str, x, y); + } + break; + + case D2DType.D2D_MEASURETEXT: + { + const codePage=wasmMem.getLong(currentInsParams+8); + const str=wasmMem.getString(wasmMem.getLong(currentInsParams), undefined, codePage); + const tmidx=wasmMem.getLong(currentInsParams+4); + + const tm=this.ctx.measureText(str); + wasmMem.setDouble(tmidx+0, tm.actualBoundingBoxAscent); + wasmMem.setDouble(tmidx+8, tm.actualBoundingBoxDescent); + wasmMem.setDouble(tmidx+16, tm.actualBoundingBoxLeft); + wasmMem.setDouble(tmidx+24, tm.actualBoundingBoxRight); + wasmMem.setDouble(tmidx+32, tm.fontBoundingBoxAscent); + wasmMem.setDouble(tmidx+40, tm.fontBoundingBoxDescent); + wasmMem.setDouble(tmidx+48, tm.width); + } + break; - case D2DType.D2D_SETLINEWIDTH: - { - const width=owner.getDouble(currentInsParams); - this.ctx.lineWidth=width; - //console.log("twrCanvas D2D_SETLINEWIDTH: ", this.ctx.lineWidth); - } - break; + case D2DType.D2D_SETFONT: + { + const fontPointer = wasmMem.getLong(currentInsParams); + const str=wasmMem.getString(fontPointer); + this.ctx.font=str; + } + break; + + case D2DType.D2D_SETFILLSTYLERGBA: + { + const color=wasmMem.getLong(currentInsParams); + const cssColor= "#"+("00000000" + color.toString(16)).slice(-8); + this.ctx.fillStyle = cssColor; + //console.log("fillstyle: ", this.ctx.fillStyle, ":", cssColor,":", color) + } + break; - case D2DType.D2D_MOVETO: - { - const x=owner.getDouble(currentInsParams); - const y=owner.getDouble(currentInsParams+8); - this.ctx.moveTo(x, y); - } - break; + case D2DType.D2D_SETSTROKESTYLERGBA: + { + const color=wasmMem.getLong(currentInsParams); + const cssColor= "#"+("00000000" + color.toString(16)).slice(-8); + this.ctx.strokeStyle = cssColor; + } + break; - case D2DType.D2D_LINETO: - { - const x=owner.getDouble(currentInsParams); - const y=owner.getDouble(currentInsParams+8); - this.ctx.lineTo(x, y); - } - break; - - case D2DType.D2D_BEZIERTO: - { - const cp1x=owner.getDouble(currentInsParams); - const cp1y=owner.getDouble(currentInsParams+8); - const cp2x=owner.getDouble(currentInsParams+16); - const cp2y=owner.getDouble(currentInsParams+24); - const x=owner.getDouble(currentInsParams+32); - const y=owner.getDouble(currentInsParams+40); - this.ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); - } - break; + case D2DType.D2D_SETFILLSTYLE: + { + const cssColorPointer = wasmMem.getLong(currentInsParams); + const cssColor= wasmMem.getString(cssColorPointer); + this.ctx.fillStyle = cssColor; + } + break - case D2DType.D2D_BEGINPATH: - { - this.ctx.beginPath(); - } - break; + case D2DType.D2D_SETSTROKESTYLE: + { + const cssColorPointer = wasmMem.getLong(currentInsParams); + const cssColor= wasmMem.getString(cssColorPointer); + this.ctx.strokeStyle = cssColor; + } + break - case D2DType.D2D_FILL: - { - this.ctx.fill(); - } - break; + case D2DType.D2D_SETLINEWIDTH: + { + const width=wasmMem.getDouble(currentInsParams); + this.ctx.lineWidth=width; + //console.log("twrCanvas D2D_SETLINEWIDTH: ", this.ctx.lineWidth); + } + break; - case D2DType.D2D_SAVE: - { - this.ctx.save(); - } - break; + case D2DType.D2D_MOVETO: + { + const x=wasmMem.getDouble(currentInsParams); + const y=wasmMem.getDouble(currentInsParams+8); + this.ctx.moveTo(x, y); + } + break; - case D2DType.D2D_RESTORE: - { - this.ctx.restore(); - } - break; + case D2DType.D2D_LINETO: + { + const x=wasmMem.getDouble(currentInsParams); + const y=wasmMem.getDouble(currentInsParams+8); + this.ctx.lineTo(x, y); + } + break; + + case D2DType.D2D_BEZIERTO: + { + const cp1x=wasmMem.getDouble(currentInsParams); + const cp1y=wasmMem.getDouble(currentInsParams+8); + const cp2x=wasmMem.getDouble(currentInsParams+16); + const cp2y=wasmMem.getDouble(currentInsParams+24); + const x=wasmMem.getDouble(currentInsParams+32); + const y=wasmMem.getDouble(currentInsParams+40); + this.ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); + } + break; - case D2DType.D2D_STROKE: - { - this.ctx.stroke(); - } - break; - - case D2DType.D2D_ARC: - { - const x=owner.getDouble(currentInsParams); - const y=owner.getDouble(currentInsParams+8); - const radius=owner.getDouble(currentInsParams+16); - const startAngle=owner.getDouble(currentInsParams+24); - const endAngle=owner.getDouble(currentInsParams+32); - const counterClockwise= (owner.getLong(currentInsParams+40)!=0); - - this.ctx.arc(x, y, radius, startAngle, endAngle, counterClockwise) - } - break; + case D2DType.D2D_BEGINPATH: + { + this.ctx.beginPath(); + } + break; - case D2DType.D2D_IMAGEDATA: - { - const start=owner.getLong(currentInsParams); - const length=owner.getLong(currentInsParams+4); - const width=owner.getLong(currentInsParams+8); - const height=owner.getLong(currentInsParams+12); - const id=owner.getLong(currentInsParams+16); + case D2DType.D2D_FILL: + { + this.ctx.fill(); + } + break; - if ( id in this.precomputedObjects ) console.log("warning: D2D_IMAGEDATA ID already exists."); + case D2DType.D2D_SAVE: + { + this.ctx.save(); + } + break; - if (this.isAsyncMod) { // Uint8ClampedArray doesn't support shared memory - this.precomputedObjects[id]={mem8: new Uint8Array(owner.memory!.buffer, start, length), width:width, height:height}; - } - else { - const z = new Uint8ClampedArray(owner.memory!.buffer, start, length); - this.precomputedObjects[id]=new ImageData(z, width, height); - } - } - break; - - case D2DType.D2D_CREATERADIALGRADIENT: - { - const x0=owner.getDouble(currentInsParams); - const y0=owner.getDouble(currentInsParams+8); - const radius0=owner.getDouble(currentInsParams+16); - const x1=owner.getDouble(currentInsParams+24); - const y1=owner.getDouble(currentInsParams+32); - const radius1=owner.getDouble(currentInsParams+40); - const id= owner.getLong(currentInsParams+48); - - let gradient=this.ctx.createRadialGradient(x0, y0, radius0, x1, y1, radius1); - if ( id in this.precomputedObjects ) console.log("warning: D2D_CREATERADIALGRADIENT ID already exists."); - this.precomputedObjects[id] = gradient; - } - break + case D2DType.D2D_RESTORE: + { + this.ctx.restore(); + } + break; - case D2DType.D2D_CREATELINEARGRADIENT: - { - const x0=owner.getDouble(currentInsParams); - const y0=owner.getDouble(currentInsParams+8); - const x1=owner.getDouble(currentInsParams+16); - const y1=owner.getDouble(currentInsParams+24); - const id= owner.getLong(currentInsParams+32); - - let gradient=this.ctx.createLinearGradient(x0, y0, x1, y1); - if ( id in this.precomputedObjects ) console.log("warning: D2D_CREATELINEARGRADIENT ID already exists."); - this.precomputedObjects[id] = gradient; - } - break + case D2DType.D2D_STROKE: + { + this.ctx.stroke(); + } + break; + + case D2DType.D2D_ARC: + { + const x=wasmMem.getDouble(currentInsParams); + const y=wasmMem.getDouble(currentInsParams+8); + const radius=wasmMem.getDouble(currentInsParams+16); + const startAngle=wasmMem.getDouble(currentInsParams+24); + const endAngle=wasmMem.getDouble(currentInsParams+32); + const counterClockwise= (wasmMem.getLong(currentInsParams+40)!=0); + + this.ctx.arc(x, y, radius, startAngle, endAngle, counterClockwise) + } + break; - case D2DType.D2D_SETCOLORSTOP: - { - const id = owner.getLong(currentInsParams); - const pos=owner.getLong(currentInsParams+4); - const cssColorPointer = owner.getLong(currentInsParams+8); - const cssColor= owner.getString(cssColorPointer); + case D2DType.D2D_IMAGEDATA: + { + const start=wasmMem.getLong(currentInsParams); + const length=wasmMem.getLong(currentInsParams+4); + const width=wasmMem.getLong(currentInsParams+8); + const height=wasmMem.getLong(currentInsParams+12); + const id=wasmMem.getLong(currentInsParams+16); - if (!(id in this.precomputedObjects)) throw new Error("D2D_SETCOLORSTOP with invalid ID: "+id); - const gradient=this.precomputedObjects[id] as CanvasGradient; - gradient.addColorStop(pos, cssColor); + if ( id in this.precomputedObjects ) console.log("warning: D2D_IMAGEDATA ID already exists."); + if (mod.isTwrWasmModuleAsync) { // Uint8ClampedArray doesn't support shared memory + this.precomputedObjects[id]={mem8: new Uint8Array(wasmMem.memory!.buffer, start, length), width:width, height:height}; } - break - - case D2DType.D2D_SETFILLSTYLEGRADIENT: - { - const id=owner.getLong(currentInsParams); - if (!(id in this.precomputedObjects)) throw new Error("D2D_SETFILLSTYLEGRADIENT with invalid ID: "+id); - const gradient=this.precomputedObjects[id] as CanvasGradient; - this.ctx.fillStyle=gradient; + else { + const z = new Uint8ClampedArray(wasmMem.memory!.buffer, start, length); + this.precomputedObjects[id]=new ImageData(z, width, height); } + } + break; + + case D2DType.D2D_CREATERADIALGRADIENT: + { + const x0=wasmMem.getDouble(currentInsParams); + const y0=wasmMem.getDouble(currentInsParams+8); + const radius0=wasmMem.getDouble(currentInsParams+16); + const x1=wasmMem.getDouble(currentInsParams+24); + const y1=wasmMem.getDouble(currentInsParams+32); + const radius1=wasmMem.getDouble(currentInsParams+40); + const id= wasmMem.getLong(currentInsParams+48); + + let gradient=this.ctx.createRadialGradient(x0, y0, radius0, x1, y1, radius1); + if ( id in this.precomputedObjects ) console.log("warning: D2D_CREATERADIALGRADIENT ID already exists."); + this.precomputedObjects[id] = gradient; + } + break + + case D2DType.D2D_CREATELINEARGRADIENT: + { + const x0=wasmMem.getDouble(currentInsParams); + const y0=wasmMem.getDouble(currentInsParams+8); + const x1=wasmMem.getDouble(currentInsParams+16); + const y1=wasmMem.getDouble(currentInsParams+24); + const id= wasmMem.getLong(currentInsParams+32); + + let gradient=this.ctx.createLinearGradient(x0, y0, x1, y1); + if ( id in this.precomputedObjects ) console.log("warning: D2D_CREATELINEARGRADIENT ID already exists."); + this.precomputedObjects[id] = gradient; + } break - case D2DType.D2D_RELEASEID: - { - const id=owner.getLong(currentInsParams); - if (this.precomputedObjects[id]) - delete this.precomputedObjects[id]; - else - console.log("warning: D2D_RELEASEID with undefined ID ",id); - } - break + case D2DType.D2D_SETCOLORSTOP: + { + const id = wasmMem.getLong(currentInsParams); + const pos=wasmMem.getLong(currentInsParams+4); + const cssColorPointer = wasmMem.getLong(currentInsParams+8); + const cssColor= wasmMem.getString(cssColorPointer); - + if (!(id in this.precomputedObjects)) throw new Error("D2D_SETCOLORSTOP with invalid ID: "+id); + const gradient=this.precomputedObjects[id] as CanvasGradient; + gradient.addColorStop(pos, cssColor); - case D2DType.D2D_PUTIMAGEDATA: - { - const id=owner.getLong(currentInsParams); - const dx=owner.getLong(currentInsParams+4); - const dy=owner.getLong(currentInsParams+8); - const dirtyX=owner.getLong(currentInsParams+12); - const dirtyY=owner.getLong(currentInsParams+16); - const dirtyWidth=owner.getLong(currentInsParams+20); - const dirtyHeight=owner.getLong(currentInsParams+24); + } + break + + case D2DType.D2D_SETFILLSTYLEGRADIENT: + { + const id=wasmMem.getLong(currentInsParams); + if (!(id in this.precomputedObjects)) throw new Error("D2D_SETFILLSTYLEGRADIENT with invalid ID: "+id); + const gradient=this.precomputedObjects[id] as CanvasGradient; + this.ctx.fillStyle=gradient; + } + break + + case D2DType.D2D_RELEASEID: + { + const id=wasmMem.getLong(currentInsParams); + if (this.precomputedObjects[id]) + delete this.precomputedObjects[id]; + else + console.log("warning: D2D_RELEASEID with undefined ID ",id); + } + break - if (!(id in this.precomputedObjects)) throw new Error("D2D_PUTIMAGEDATA with invalid ID: "+id); + - //console.log("D2D_PUTIMAGEDATA",start, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight, this.imageData[start]); + case D2DType.D2D_PUTIMAGEDATA: + { + const id=wasmMem.getLong(currentInsParams); + const dx=wasmMem.getLong(currentInsParams+4); + const dy=wasmMem.getLong(currentInsParams+8); + const dirtyX=wasmMem.getLong(currentInsParams+12); + const dirtyY=wasmMem.getLong(currentInsParams+16); + const dirtyWidth=wasmMem.getLong(currentInsParams+20); + const dirtyHeight=wasmMem.getLong(currentInsParams+24); - let imgData:ImageData; - - if (this.isAsyncMod) { // Uint8ClampedArray doesn't support shared memory, so copy the memory - //console.log("D2D_PUTIMAGEDATA wasmModuleAsync"); - const z = this.precomputedObjects[id] as {mem8:Uint8Array, width:number, height:number}; // Uint8Array - const ca=Uint8ClampedArray.from(z.mem8); // shallow copy - imgData=new ImageData(ca, z.width, z.height); - } - else { - imgData=this.precomputedObjects[id] as ImageData; - } - - if (dirtyWidth==0 && dirtyHeight==0) { - this.ctx.putImageData(imgData, dx, dy); - } - else { - this.ctx.putImageData(imgData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight); - } - } - break; + if (!(id in this.precomputedObjects)) throw new Error("D2D_PUTIMAGEDATA with invalid ID: "+id); - case D2DType.D2D_CLOSEPATH: - { - this.ctx.closePath(); - } - break; - - case D2DType.D2D_RESET: - { - this.ctx.reset(); - } - break; - - case D2DType.D2D_CLEARRECT: - { - const x=owner.getDouble(currentInsParams); - const y=owner.getDouble(currentInsParams+8); - const w=owner.getDouble(currentInsParams+16); - const h=owner.getDouble(currentInsParams+24); - this.ctx.clearRect(x, y, w, h); - } - break; - - case D2DType.D2D_SCALE: - { - const x=owner.getDouble(currentInsParams); - const y=owner.getDouble(currentInsParams+8); - this.ctx.scale(x, y); - } - break; - - case D2DType.D2D_TRANSLATE: - { - const x=owner.getDouble(currentInsParams); - const y=owner.getDouble(currentInsParams+8); - this.ctx.translate(x, y); - } - break; - - case D2DType.D2D_ROTATE: - { - const angle=owner.getDouble(currentInsParams); - this.ctx.rotate(angle); - } - break; - - case D2DType.D2D_GETTRANSFORM: - { - const matrix_ptr=owner.getLong(currentInsParams); - const transform=this.ctx.getTransform(); - owner.setDouble(matrix_ptr+0, transform.a); - owner.setDouble(matrix_ptr+8, transform.b); - owner.setDouble(matrix_ptr+16, transform.c); - owner.setDouble(matrix_ptr+24, transform.d); - owner.setDouble(matrix_ptr+32, transform.e); - owner.setDouble(matrix_ptr+40, transform.f); - } - break; - - case D2DType.D2D_SETTRANSFORM: - { - const a = owner.getDouble(currentInsParams); - const b = owner.getDouble(currentInsParams+8); - const c = owner.getDouble(currentInsParams+16); - const d = owner.getDouble(currentInsParams+24); - const e = owner.getDouble(currentInsParams+32); - const f = owner.getDouble(currentInsParams+40); - - this.ctx.setTransform(a, b, c, d, e, f); - } - break; - - case D2DType.D2D_RESETTRANSFORM: - { - this.ctx.resetTransform(); - } - break; - - case D2DType.D2D_STROKETEXT: - { - const x=owner.getDouble(currentInsParams); - const y=owner.getDouble(currentInsParams+8); - const codePage=owner.getLong(currentInsParams+20); - const strPointer = owner.getLong(currentInsParams+16); - const str=owner.getString(strPointer, undefined, codePage); + //console.log("D2D_PUTIMAGEDATA",start, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight, this.imageData[start]); + + let imgData:ImageData; - this.ctx.strokeText(str, x, y); + if (mod.isTwrWasmModuleAsync) { // Uint8ClampedArray doesn't support shared memory, so copy the memory + //console.log("D2D_PUTIMAGEDATA wasmModuleAsync"); + const z = this.precomputedObjects[id] as {mem8:Uint8Array, width:number, height:number}; // Uint8Array + const ca=Uint8ClampedArray.from(z.mem8); // shallow copy + imgData=new ImageData(ca, z.width, z.height); } - break; - - case D2DType.D2D_ROUNDRECT: - { - const x = owner.getDouble(currentInsParams); - const y = owner.getDouble(currentInsParams+8); - const width = owner.getDouble(currentInsParams+16); - const height = owner.getDouble(currentInsParams+24); - const radii = owner.getDouble(currentInsParams+32); - - this.ctx.roundRect(x, y, width, height, radii); + else { + imgData=this.precomputedObjects[id] as ImageData; } - break; - case D2DType.D2D_ELLIPSE: - { - const x=owner.getDouble(currentInsParams); - const y=owner.getDouble(currentInsParams+8); - const radiusX=owner.getDouble(currentInsParams+16); - const radiusY=owner.getDouble(currentInsParams+24); - const rotation=owner.getDouble(currentInsParams+32); - const startAngle=owner.getDouble(currentInsParams+40); - const endAngle=owner.getDouble(currentInsParams+48); - const counterClockwise= (owner.getLong(currentInsParams+56)!=0); - - this.ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterClockwise) + if (dirtyWidth==0 && dirtyHeight==0) { + this.ctx.putImageData(imgData, dx, dy); } - break; - - case D2DType.D2D_QUADRATICCURVETO: - { - const cpx = owner.getDouble(currentInsParams); - const cpy = owner.getDouble(currentInsParams+8); - const x = owner.getDouble(currentInsParams+16); - const y = owner.getDouble(currentInsParams+24); - - this.ctx.quadraticCurveTo(cpx, cpy, x, y); + else { + this.ctx.putImageData(imgData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight); } - break; + } + break; + + case D2DType.D2D_CLOSEPATH: + { + this.ctx.closePath(); + } + break; + + case D2DType.D2D_RESET: + { + this.ctx.reset(); + } + break; + + case D2DType.D2D_CLEARRECT: + { + const x=wasmMem.getDouble(currentInsParams); + const y=wasmMem.getDouble(currentInsParams+8); + const w=wasmMem.getDouble(currentInsParams+16); + const h=wasmMem.getDouble(currentInsParams+24); + this.ctx.clearRect(x, y, w, h); + } + break; + + case D2DType.D2D_SCALE: + { + const x=wasmMem.getDouble(currentInsParams); + const y=wasmMem.getDouble(currentInsParams+8); + this.ctx.scale(x, y); + } + break; + + case D2DType.D2D_TRANSLATE: + { + const x=wasmMem.getDouble(currentInsParams); + const y=wasmMem.getDouble(currentInsParams+8); + this.ctx.translate(x, y); + } + break; - case D2DType.D2D_SETLINEDASH: - { - const segment_len = owner.getLong(currentInsParams); - const seg_ptr = owner.getLong(currentInsParams+4); - let segments = []; - for (let i = 0; i < segment_len; i++) { - segments[i] = owner.getDouble(seg_ptr + i*8); + case D2DType.D2D_ROTATE: + { + const angle=wasmMem.getDouble(currentInsParams); + this.ctx.rotate(angle); + } + break; + + case D2DType.D2D_GETTRANSFORM: + { + const matrix_ptr=wasmMem.getLong(currentInsParams); + const transform=this.ctx.getTransform(); + wasmMem.setDouble(matrix_ptr+0, transform.a); + wasmMem.setDouble(matrix_ptr+8, transform.b); + wasmMem.setDouble(matrix_ptr+16, transform.c); + wasmMem.setDouble(matrix_ptr+24, transform.d); + wasmMem.setDouble(matrix_ptr+32, transform.e); + wasmMem.setDouble(matrix_ptr+40, transform.f); + } + break; + + case D2DType.D2D_SETTRANSFORM: + { + const a = wasmMem.getDouble(currentInsParams); + const b = wasmMem.getDouble(currentInsParams+8); + const c = wasmMem.getDouble(currentInsParams+16); + const d = wasmMem.getDouble(currentInsParams+24); + const e = wasmMem.getDouble(currentInsParams+32); + const f = wasmMem.getDouble(currentInsParams+40); + + this.ctx.setTransform(a, b, c, d, e, f); + } + break; + + case D2DType.D2D_RESETTRANSFORM: + { + this.ctx.resetTransform(); + } + break; + + case D2DType.D2D_STROKETEXT: + { + const x=wasmMem.getDouble(currentInsParams); + const y=wasmMem.getDouble(currentInsParams+8); + const codePage=wasmMem.getLong(currentInsParams+20); + const strPointer = wasmMem.getLong(currentInsParams+16); + const str=wasmMem.getString(strPointer, undefined, codePage); + + this.ctx.strokeText(str, x, y); + } + break; + + case D2DType.D2D_ROUNDRECT: + { + const x = wasmMem.getDouble(currentInsParams); + const y = wasmMem.getDouble(currentInsParams+8); + const width = wasmMem.getDouble(currentInsParams+16); + const height = wasmMem.getDouble(currentInsParams+24); + const radii = wasmMem.getDouble(currentInsParams+32); + + this.ctx.roundRect(x, y, width, height, radii); + } + break; + + case D2DType.D2D_ELLIPSE: + { + const x=wasmMem.getDouble(currentInsParams); + const y=wasmMem.getDouble(currentInsParams+8); + const radiusX=wasmMem.getDouble(currentInsParams+16); + const radiusY=wasmMem.getDouble(currentInsParams+24); + const rotation=wasmMem.getDouble(currentInsParams+32); + const startAngle=wasmMem.getDouble(currentInsParams+40); + const endAngle=wasmMem.getDouble(currentInsParams+48); + const counterClockwise= (wasmMem.getLong(currentInsParams+56)!=0); + + this.ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterClockwise) + } + break; + + case D2DType.D2D_QUADRATICCURVETO: + { + const cpx = wasmMem.getDouble(currentInsParams); + const cpy = wasmMem.getDouble(currentInsParams+8); + const x = wasmMem.getDouble(currentInsParams+16); + const y = wasmMem.getDouble(currentInsParams+24); + + this.ctx.quadraticCurveTo(cpx, cpy, x, y); + } + break; + + case D2DType.D2D_SETLINEDASH: + { + const segment_len = wasmMem.getLong(currentInsParams); + const seg_ptr = wasmMem.getLong(currentInsParams+4); + let segments = []; + for (let i = 0; i < segment_len; i++) { + segments[i] = wasmMem.getDouble(seg_ptr + i*8); + } + this.ctx.setLineDash(segments); + } + break; + + case D2DType.D2D_GETLINEDASH: + { + const segments = this.ctx.getLineDash(); + + const buffer_length = wasmMem.getLong(currentInsParams); + const buffer_ptr = wasmMem.getLong(currentInsParams+4); + const segment_length_ptr = currentInsParams+8; + + wasmMem.setLong(segment_length_ptr, segments.length); + if (segments.length > 0) { + for (let i = 0; i < Math.min(segments.length, buffer_length); i++) { + wasmMem.setDouble(buffer_ptr + i*8, segments[i]); } - this.ctx.setLineDash(segments); - } - break; - - case D2DType.D2D_GETLINEDASH: - { - const segments = this.ctx.getLineDash(); - - const buffer_length = owner.getLong(currentInsParams); - const buffer_ptr = owner.getLong(currentInsParams+4); - const segment_length_ptr = currentInsParams+8; - - owner.setLong(segment_length_ptr, segments.length); - if (segments.length > 0) { - for (let i = 0; i < Math.min(segments.length, buffer_length); i++) { - owner.setDouble(buffer_ptr + i*8, segments[i]); - } - if (segments.length > buffer_length) { - console.log("warning: D2D_GETLINEDASH exceeded given max_length, truncating excess"); - } + if (segments.length > buffer_length) { + console.log("warning: D2D_GETLINEDASH exceeded given max_length, truncating excess"); } } - break; - - case D2DType.D2D_ARCTO: - { - const x1 = owner.getDouble(currentInsParams); - const y1 = owner.getDouble(currentInsParams+8); - const x2 = owner.getDouble(currentInsParams+16); - const y2 = owner.getDouble(currentInsParams+24); - const radius = owner.getDouble(currentInsParams+32); - - this.ctx.arcTo(x1, y1, x2, y2, radius); - } - break; - - case D2DType.D2D_GETLINEDASHLENGTH: - { - owner.setLong(currentInsParams, this.ctx.getLineDash().length); - } - break; - - case D2DType.D2D_DRAWIMAGE: - { - const dx = owner.getDouble(currentInsParams); - const dy = owner.getDouble(currentInsParams+8); - const id = owner.getLong(currentInsParams+16); + } + break; + + case D2DType.D2D_ARCTO: + { + const x1 = wasmMem.getDouble(currentInsParams); + const y1 = wasmMem.getDouble(currentInsParams+8); + const x2 = wasmMem.getDouble(currentInsParams+16); + const y2 = wasmMem.getDouble(currentInsParams+24); + const radius = wasmMem.getDouble(currentInsParams+32); + + this.ctx.arcTo(x1, y1, x2, y2, radius); + } + break; + + case D2DType.D2D_GETLINEDASHLENGTH: + { + wasmMem.setLong(currentInsParams, this.ctx.getLineDash().length); + } + break; + + case D2DType.D2D_DRAWIMAGE: + { + const sx = wasmMem.getDouble(currentInsParams); + const sy = wasmMem.getDouble(currentInsParams+8); + + const sWidth = wasmMem.getDouble(currentInsParams+16); + const sHeight = wasmMem.getDouble(currentInsParams+24); - if (!(id in this.precomputedObjects)) throw new Error("D2D_DRAWIMAGE with invalid ID: "+id); + const dx = wasmMem.getDouble(currentInsParams+32); + const dy = wasmMem.getDouble(currentInsParams+40); - let img = this.precomputedObjects[id] as HTMLImageElement; + const dWidth = wasmMem.getDouble(currentInsParams+48); + const dHeight = wasmMem.getDouble(currentInsParams+56); + + const id = wasmMem.getLong(currentInsParams+64); + + if (!(id in this.precomputedObjects)) throw new Error("D2D_DRAWIMAGE with invalid ID: "+id); + + let img = this.precomputedObjects[id] as HTMLImageElement; + + if (sWidth == 0 && sHeight == 0 && dWidth == 0 && dHeight == 0) { this.ctx.drawImage(img, dx, dy); + } else if (dWidth == 0 && dHeight == 0) { + this.ctx.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, sWidth, sHeight); + } else if (sWidth == 0 && sHeight == 0) { + this.ctx.drawImage(img, sx, sy, img.width, img.height, dx, dy, dWidth, dHeight); + }else { + this.ctx.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); } - break; - - case D2DType.D2D_RECT: - { - const x = owner.getDouble(currentInsParams); - const y = owner.getDouble(currentInsParams+8); - const width = owner.getDouble(currentInsParams+16); - const height = owner.getDouble(currentInsParams+24); - - this.ctx.rect(x, y, width, height); - } - break; - - case D2DType.D2D_TRANSFORM: - { - const a = owner.getDouble(currentInsParams); - const b = owner.getDouble(currentInsParams+8); - const c = owner.getDouble(currentInsParams+16); - const d = owner.getDouble(currentInsParams+24); - const e = owner.getDouble(currentInsParams+32); - const f = owner.getDouble(currentInsParams+40); - - this.ctx.transform(a, b, c, d, e, f); - } - break; - - case D2DType.D2D_SETLINECAP: - { - const lineCapPtr = owner.getLong(currentInsParams); - const lineCap = owner.getString(lineCapPtr); + } + break; + + case D2DType.D2D_RECT: + { + const x = wasmMem.getDouble(currentInsParams); + const y = wasmMem.getDouble(currentInsParams+8); + const width = wasmMem.getDouble(currentInsParams+16); + const height = wasmMem.getDouble(currentInsParams+24); + + this.ctx.rect(x, y, width, height); + } + break; + + case D2DType.D2D_TRANSFORM: + { + const a = wasmMem.getDouble(currentInsParams); + const b = wasmMem.getDouble(currentInsParams+8); + const c = wasmMem.getDouble(currentInsParams+16); + const d = wasmMem.getDouble(currentInsParams+24); + const e = wasmMem.getDouble(currentInsParams+32); + const f = wasmMem.getDouble(currentInsParams+40); + + this.ctx.transform(a, b, c, d, e, f); + } + break; + + case D2DType.D2D_SETLINECAP: + { + const lineCapPtr = wasmMem.getLong(currentInsParams); + const lineCap = wasmMem.getString(lineCapPtr); + + this.ctx.lineCap = lineCap as CanvasLineCap; + } + break; - this.ctx.lineCap = lineCap as CanvasLineCap; - } - break; + case D2DType.D2D_SETLINEJOIN: + { + const lineJoinPtr = wasmMem.getLong(currentInsParams); + const lineJoin = wasmMem.getString(lineJoinPtr); - case D2DType.D2D_SETLINEJOIN: - { - const lineJoinPtr = owner.getLong(currentInsParams); - const lineJoin = owner.getString(lineJoinPtr); + this.ctx.lineJoin = lineJoin as CanvasLineJoin; + } + break; + + case D2DType.D2D_SETLINEDASHOFFSET: + { + const lineDashOffset = wasmMem.getDouble(currentInsParams); - this.ctx.lineJoin = lineJoin as CanvasLineJoin; - } - break; + this.ctx.lineDashOffset = lineDashOffset; + } + break; + + case D2DType.D2D_GETIMAGEDATA: + { + const x = wasmMem.getDouble(currentInsParams); + const y = wasmMem.getDouble(currentInsParams+8); + const width = wasmMem.getDouble(currentInsParams+16); + const height = wasmMem.getDouble(currentInsParams+24); + const id = wasmMem.getLong(currentInsParams+32); - case D2DType.D2D_SETLINEDASHOFFSET: - { - const lineDashOffset = owner.getDouble(currentInsParams); + const imgData = this.ctx.getImageData(x, y, width, height); - this.ctx.lineDashOffset = lineDashOffset; - } - break; - - case D2DType.D2D_GETIMAGEDATA: - { - const x = owner.getDouble(currentInsParams); - const y = owner.getDouble(currentInsParams+8); - const width = owner.getDouble(currentInsParams+16); - const height = owner.getDouble(currentInsParams+24); - - const memPtr = owner.getLong(currentInsParams+32); - const memLen = owner.getLong(currentInsParams+36); - - let imgData = this.ctx.getImageData(x, y, width, height); - const imgLen = imgData.data.byteLength; - console.log("img len: ", imgLen); - if (imgLen > memLen) console.log("Warning: D2D_GETIMAGEDATA was given a buffer smaller than the image size! Extra data is being truncated"); - owner.mem8.set(imgData.data.slice(0, Math.min(memLen, imgLen)), memPtr); - } - break; + if ( id in this.precomputedObjects ) console.log("warning: D2D_GETIMAGEDATA ID already exists."); + this.precomputedObjects[id] = imgData; - default: - throw new Error ("unimplemented or unknown Sequence Type in drawSeq: "+type); + // const memPtr = wasmMem.getLong(currentInsParams+32); + // const memLen = wasmMem.getLong(currentInsParams+36); + + // let imgData = this.ctx.getImageData(x, y, width, height); + // const imgLen = imgData.data.byteLength; + // if (imgLen > memLen) console.log("Warning: D2D_GETIMAGEDATA was given a buffer smaller than the image size! Extra data is being truncated"); + // owner.mem8.set(imgData.data.slice(0, Math.min(memLen, imgLen)), memPtr); } - nextInsHdr=owner.getLong(currentInsHdr); /* hdr->next */ - if (nextInsHdr==0) { - if (currentInsHdr!=lastInsHdr) throw new Error("assert type error in twrcanvas, ins!=lastins"); - break; + break; + + case D2DType.D2D_IMAGEDATATOC: + { + const bufferPtr = wasmMem.getLong(currentInsParams); + const bufferLen = wasmMem.getLong(currentInsParams+4); + const id = wasmMem.getLong(currentInsParams+8); + + if (!(id in this.precomputedObjects)) throw new Error("D2D_IMAGEDATATOC with invalid ID: "+id); + + const img = this.precomputedObjects[id] as ImageData; + const imgLen = img.data.byteLength; + if (imgLen > bufferLen) console.log("Warning: D2D_IMAGEDATATOC was given a buffer smaller than the image size! Extra data is being truncated"); + wasmMem.mem8.set(img.data.slice(0, Math.min(bufferLen, imgLen)), bufferPtr); + } + break; + + case D2DType.D2D_GETCANVASPROPDOUBLE: + { + const valPtr = wasmMem.getLong(currentInsParams); + const namePtr = wasmMem.getLong(currentInsParams+4); + + const propName = wasmMem.getString(namePtr); + + const val = (this.ctx as {[key: string]: any})[propName]; + if (typeof val != "number") throw new Error("D2D_GETCANVASPROPDOUBLE with property " + propName + " expected a number, got " + (typeof val) + "!"); + wasmMem.setDouble(valPtr, val); } - currentInsHdr=nextInsHdr; - currentInsParams = currentInsHdr + insHdrSize; - } + break; + + case D2DType.D2D_GETCANVASPROPSTRING: + { + const valPtr = wasmMem.getLong(currentInsParams); + const valMaxLen = wasmMem.getLong(currentInsParams+4); + const namePtr = wasmMem.getLong(currentInsParams+8); + + const propName = wasmMem.getString(namePtr); + + const val = (this.ctx as {[key: string]: any})[propName]; + if (typeof val != "string") throw new Error("D2D_GETCANVASPROPSTRING with property " + propName + " expected a string, got " + (typeof val) + "!"); + + const encodedVal = wasmMem.stringToU8(val as string); + if (encodedVal.byteLength >= valMaxLen) console.log("Warning: D2D_GETCANVASPROPSTRING was given a buffer smaller than the return value! The extra data is being truncated!"); + + const strLen = Math.min(encodedVal.byteLength, valMaxLen-1); //-1 from valMaxLen for null character + wasmMem.mem8.set(encodedVal.slice(0, strLen), valPtr); + wasmMem.mem8[strLen + valPtr] = 0; //ensure the null character gets set + } + break; + + case D2DType.D2D_SETCANVASPROPDOUBLE: + { + const val = wasmMem.getDouble(currentInsParams); + const namePtr = wasmMem.getLong(currentInsParams+8); - if (this.cmdCompleteSignal) this.cmdCompleteSignal.signal(); - //console.log("Canvas.drawSeq() completed with instruction count of ", insCount); - } -} + const propName = wasmMem.getString(namePtr); -export class twrConsoleCanvasProxy implements IConsoleCanvasProxy { - canvasKeys: twrSharedCircularBuffer; - drawCompleteSignal:twrSignal; - props: ICanvasProps; - id:number; - returnValue: twrSharedCircularBuffer; + const prevVal = (this.ctx as {[key: string]: any})[propName]; + if (typeof prevVal != "number") throw new Error("D2D_SETCANVASPROPDOUBLE with property " + propName + " expected a number, got " + (typeof prevVal) + "!"); + + (this.ctx as {[key: string]: any})[propName] = val; + } + break; + + case D2DType.D2D_SETCANVASPROPSTRING: + { + const valPtr = wasmMem.getLong(currentInsParams); + const namePtr = wasmMem.getLong(currentInsParams+4); - constructor(params:TConsoleCanvasProxyParams) { - const [className, id, props, signalBuffer, canvasKeysBuffer, returnBuffer] = params; - this.drawCompleteSignal = new twrSignal(signalBuffer); - this.canvasKeys = new twrSharedCircularBuffer(canvasKeysBuffer); - this.returnValue = new twrSharedCircularBuffer(returnBuffer); - this.props=props; - this.id=id; + const val = wasmMem.getString(valPtr); + const propName = wasmMem.getString(namePtr); - //console.log("Create New twrCanvasProxy: ",this.props) + const prevVal = (this.ctx as {[key: string]: any})[propName]; + if (typeof prevVal != "string") throw new Error("D2D_SETCANVASPROPSTRING with property " + propName + " expected a string, got " + (typeof prevVal) + "!"); + (this.ctx as {[key: string]: any})[propName] = val; + } + break; + + default: + throw new Error ("unimplemented or unknown Sequence Type in drawSeq: "+type); + } + nextInsHdr=wasmMem.getLong(currentInsHdr); /* hdr->next */ + if (nextInsHdr==0) { + if (currentInsHdr!=lastInsHdr) throw new Error("assert type error in twrcanvas, ins!=lastins"); + break; + } + currentInsHdr=nextInsHdr; + currentInsParams = currentInsHdr + insHdrSize; + } } - charIn() { - //ctx.commit(); not avail in chrome +} + +export default twrConsoleCanvas; - //postMessage(["debug", 'x']); - - return this.canvasKeys.readWait(); // wait for a key, then read it - } - inkey() { - if (this.canvasKeys.isEmpty()) - return 0; - else - return this.charIn(); - } - // note that this implementation does not allow a property to change post creation of an instance of this class - getProp(propName:keyof ICanvasProps): number { - return this.props[propName]; - } - drawSeq(ds:number) { - this.drawCompleteSignal.reset(); - postMessage(["canvas2d-drawseq", [this.id, ds]]); - this.drawCompleteSignal.wait(); - } - loadImage(urlPtr: number, id: number): number { - postMessage(["canvas2d-loadimage", [this.id, urlPtr, id]]); - return this.returnValue.readWait(); - } -} diff --git a/source/twr-ts/twrcondebug.ts b/source/twr-ts/twrcondebug.ts index bddaddf9..b3e651d1 100644 --- a/source/twr-ts/twrcondebug.ts +++ b/source/twr-ts/twrcondebug.ts @@ -1,115 +1,77 @@ -import {IConsoleStream, IConsoleStreamProxy, TConsoleDebugProxyParams, IOTypes} from "./twrcon.js" -import {twrCodePageToUnicodeCodePoint, codePageUTF32} from "./twrlocale.js" -import {twrConsoleRegistry} from "./twrconreg.js" -import {twrWasmModuleBase} from "./twrmodbase.js" - -export class twrConsoleDebug implements IConsoleStream { - logline=""; - element=undefined; - id:number; - cpTranslate:twrCodePageToUnicodeCodePoint; +import {IConsoleStreamOut, IOTypes} from "./twrcon.js" +import {twrCodePageToUnicodeCodePoint} from "./twrliblocale.js" +import {IWasmModuleAsync} from "./twrmodasync.js"; +import {IWasmModule} from "./twrmod.js" +import {twrLibrary, TLibImports, twrLibraryInstanceRegistry} from "./twrlibrary.js"; + +export class twrConsoleDebug extends twrLibrary implements IConsoleStreamOut { + id:number; + logline=""; + element=undefined; + cpTranslate:twrCodePageToUnicodeCodePoint; + + imports:TLibImports = { + twrConCharOut:{noBlock:true}, + twrConGetProp:{}, + twrConPutStr:{noBlock:true}, + }; + + libSourcePath = new URL(import.meta.url).pathname; + interfaceName = "twrConsole"; + + constructor() { + // all library constructors should start with these two lines + super(); + this.id=twrLibraryInstanceRegistry.register(this); + + this.cpTranslate=new twrCodePageToUnicodeCodePoint(); + } - constructor() { - this.id=twrConsoleRegistry.registerConsole(this); - this.cpTranslate=new twrCodePageToUnicodeCodePoint(); - } + charOut(ch:string) { + if (ch.length>1) + throw new Error("charOut takes an empty string or a single char string"); + + if (ch==='\n') { + console.log(this.logline); // ideally without a linefeed, but there is no way to not have a LF with console.log API. + this.logline=""; + } + else { + this.logline=this.logline+ch; + if (this.logline.length>=300) { + console.log(this.logline); + this.logline=""; + } + } + } - charOut(ch:number, codePage:number) { + twrConCharOut(callingMod:IWasmModule|IWasmModuleAsync, ch:number, codePage:number) { const char=this.cpTranslate.convert(ch, codePage); + if (char>0) + this.charOut(String.fromCodePoint(char)); + } - if (char==10 || char==0x03) { // ASCII 03 is End-of-Text, and is used here to indicate the preceding char should be printed - console.log(this.logline); // ideally without a linefeed, but there is no way to not have a LF with console.log API. - this.logline=""; - } - else { - this.logline=this.logline+String.fromCodePoint(char); - if (this.logline.length>=300) { - console.log(this.logline); - this.logline=""; - } - } - } - - getProp(propName: string):number { - if (propName==="type") return IOTypes.CHARWRITE; - console.log("twrConsoleDebug.getProp passed unknown property name: ", propName) - return 0; - } - - getProxyParams() : TConsoleDebugProxyParams { - return ["twrConsoleDebugProxy", this.id]; - } - - keyDown(ev:KeyboardEvent) { - throw new Error("twrConsoleDebug does not support character input"); - } - - processMessage(msgType:string, data:[number, ...any[]], callingModule:twrWasmModuleBase):boolean { - const [id, ...params] = data; - if (id!=this.id) throw new Error("internal error"); // should never happen - - switch (msgType) { - case "debug-charout": - { - const [ch, codePage] = params; - this.charOut(ch, codePage); - } - break; - - case "debug-putstr": - { - const [str] = params; - this.putStr(str); - } - break; - - default: - return false; - } - - return true; - } - - putStr(str:string) { - for (let i=0; i < str.length; i++) - this.charOut(str.codePointAt(i)||0, codePageUTF32); - } -} - - -export class twrConsoleDebugProxy implements IConsoleStreamProxy { - id:number; - - constructor(params:TConsoleDebugProxyParams) { - this.id=params[1]; - } - - charIn() { - return 0; - } + getProp(propName: string):number { + if (propName==="type") return IOTypes.CHARWRITE; + console.log("twrConsoleDebug.getProp passed unknown property name: ", propName) + return 0; + } - setFocus() { + twrConGetProp(callingMod:IWasmModule|IWasmModuleAsync, pn:number):number { + const propName=callingMod.wasmMem.getString(pn); + return this.getProp(propName); + } + + putStr(str:string) { + for (let i=0; i < str.length; i++) + this.charOut(str[i]); } - - charOut(ch:number, codePoint:number) { - postMessage(["debug-charout", [this.id, ch, codePoint]]); - } - putStr(str:string):void - { - postMessage(["debug-putstr", [this.id, str]]); - } + twrConPutStr(callingMod:IWasmModule|IWasmModuleAsync, chars:number, codePage:number) { + this.putStr(callingMod.wasmMem.getString(chars, undefined, codePage)); + } - getProp(propName: string) { - if (propName==="type") return IOTypes.CHARWRITE; - console.log("twrConsoleDebugProxy.getProp passed unknown property name: ", propName) - return 0; - } } - -// ************************************************************************ -// debugLog doesn't currently wait for the message to log, it returns immediately. -// I could move this to be in the twrWaitingCalls class +export default twrConsoleDebug; diff --git a/source/twr-ts/twrcondiv.ts b/source/twr-ts/twrcondiv.ts index 25f6877c..88181c99 100644 --- a/source/twr-ts/twrcondiv.ts +++ b/source/twr-ts/twrcondiv.ts @@ -1,33 +1,55 @@ -import {twrSharedCircularBuffer} from "./twrcircular.js"; -import {twrCodePageToUnicodeCodePoint, codePageUTF32} from "./twrlocale.js" -import {IConsoleDiv, IConsoleDivProxy, IConsoleDivParams, TConsoleDivProxyParams, IOTypes, keyDownUtil} from "./twrcon.js" -import {twrConsoleRegistry} from "./twrconreg.js" +import {codePageUTF32, twrCodePageToUnicodeCodePoint} from "./twrliblocale.js" +import {IConsoleDiv, IConsoleDivParams, IOTypes, keyEventToCodePoint} from "./twrcon.js" +import {IWasmModuleAsync} from "./twrmodasync.js"; +import {IWasmModule} from "./twrmod.js" +import {twrLibrary, TLibImports, twrLibraryInstanceRegistry} from "./twrlibrary.js"; -export class twrConsoleDiv implements IConsoleDiv { - element:HTMLDivElement; +export class twrConsoleDiv extends twrLibrary implements IConsoleDiv { id:number; - keys?:twrSharedCircularBuffer; + element?:HTMLDivElement; CURSOR=String.fromCharCode(9611); // ▋ see https://daniel-hug.github.io/characters/#k_70 cursorOn:boolean=false; lastChar:number=0; extraBR:boolean=false; - cpTranslate:twrCodePageToUnicodeCodePoint; - - - constructor(element:HTMLDivElement, params:IConsoleDivParams) { - this.element=element; - - if (!(element && element instanceof HTMLDivElement)) - throw new Error("Invalid HTMLDivElement parameter in twrConsoleDiv constructor "); + cpTranslate?:twrCodePageToUnicodeCodePoint; + keyBuffer:KeyboardEvent[]=[]; + keyWaiting?:(key:number)=>void; + + imports:TLibImports = { + twrConCharOut:{noBlock:true}, + twrConGetProp:{}, + twrConPutStr:{noBlock:true}, + twrConCharIn:{isAsyncFunction: true, isModuleAsyncOnly: true}, + twrConSetFocus:{noBlock:true}, + }; + + libSourcePath = new URL(import.meta.url).pathname; + interfaceName = "twrConsole"; + + constructor(element?:HTMLDivElement, params?:IConsoleDivParams) { + // all library constructors should start with these two lines + super(); + this.id=twrLibraryInstanceRegistry.register(this); + + // twrLibraryProxy will construct with no element or params. + // this is triggered by defining a function as isCommonCode. + // Such functions should work with undefined constructor args + // TODO!! Doc this issue + if (element!==undefined) { + + this.element=element; + + if (!(element && element instanceof HTMLDivElement)) + throw new Error("Invalid HTMLDivElement parameter in twrConsoleDiv constructor "); + + if (params) { + if (params.backColor) this.element.style.backgroundColor = params.backColor; + if (params.foreColor) this.element.style.color = params.foreColor; + if (params.fontSize) this.element.style.font=params.fontSize.toString()+"px arial"; + } - if (params) { - if (params.backColor) this.element.style.backgroundColor = params.backColor; - if (params.foreColor) this.element.style.color = params.foreColor; - if (params.fontSize) this.element.style.font=params.fontSize.toString()+"px arial"; + this.cpTranslate=new twrCodePageToUnicodeCodePoint(); } - - this.cpTranslate=new twrCodePageToUnicodeCodePoint(); - this.id=twrConsoleRegistry.registerConsole(this); } private isHtmlEntityAtEnd(str:string) { @@ -35,9 +57,21 @@ export class twrConsoleDiv implements IConsoleDiv { return entityPattern.test(str); } - private removeHtmlEntityAtEnd(str:string) { - const entityPattern = /&[^;]+;$/; - return str.replace(entityPattern, ''); + private removeHtmlEntityAtEnd(str:string) { + const entityPattern = /&[^;]+;$/; + return str.replace(entityPattern, ''); + } + + twrConSetFocus() { + if (this.element===undefined) throw new Error("undefined HTMLDivElement"); + this.element.focus(); + } + + charOut(str:string) { + if (str.length>1) + throw new Error("charOut takes an empty or single char string"); + + this.twrConCharOut(undefined, str.codePointAt(0)||0, codePageUTF32); } /* @@ -47,9 +81,10 @@ export class twrConsoleDiv implements IConsoleDiv { * 0xE cursor on * 0xF cursor off */ - charOut(ch:number, codePage:number) { + twrConCharOut(callingMod:IWasmModule|IWasmModuleAsync|undefined, ch:number, codePage:number) { - if (!this.element) throw new Error("internal error"); + if (!this.element) throw new Error("undefined HTMLDivElement"); + if (!this.cpTranslate) throw new Error("internal error"); //console.log("div::charout: ", ch, codePage); @@ -113,99 +148,59 @@ export class twrConsoleDiv implements IConsoleDiv { } } + twrConGetProp(callingMod:IWasmModule|IWasmModuleAsync, pn:number):number { + const propName=callingMod.wasmMem.getString(pn); + return this.getProp(propName); + } + getProp(propName: string):number { if (propName==="type") return IOTypes.CHARWRITE|IOTypes.CHARREAD; console.log("twrConsoleDiv.getProp passed unknown property name: ", propName) return 0; } - getProxyParams() : TConsoleDivProxyParams { - this.keys = new twrSharedCircularBuffer(); // tsconfig, lib must be set to 2017 or higher - return ["twrConsoleDivProxy", this.id, this.keys.sharedArray]; - } - keyDown(ev:KeyboardEvent) { - keyDownUtil(this, ev); - } - - - processMessage(msgType:string, data:[number, ...any[]]):boolean { - const [id, ...params] = data; - if (id!=this.id) throw new Error("internal error"); // should never happen - - switch (msgType) { - case "div-charout": - { - const [ch, codePage] = params; - this.charOut(ch, codePage); - } - break; - - case "div-putstr": - { - const [str] = params; - this.putStr(str); + if (this.keyWaiting) { + const r=keyEventToCodePoint(ev); + if (r) { + this.keyWaiting(r); + this.keyWaiting=undefined; } - break; + } + else { + this.keyBuffer.push(ev); + } + } - case "div-focus": - { - this.element.focus(); + // TODO!! Should keyBuffer be flushed? Is keyBuffer needed? + async twrConCharIn_async(callingMod: IWasmModuleAsync):Promise { + let ev:KeyboardEvent|undefined; + + return new Promise( (resolve) => { + if (this.keyWaiting) + throw new Error("internal error"); + while (ev=this.keyBuffer.shift()) { + const r=keyEventToCodePoint(ev); + if (r) { + resolve(r); + return; + } } - break; - default: - return false; - } + this.keyWaiting=resolve; - return true; + }); } putStr(str:string) { for (let i=0; i < str.length; i++) - this.charOut(str.codePointAt(i)||0, codePageUTF32); + this.twrConCharOut(undefined, str.codePointAt(i)!, codePageUTF32); } -} - -export class twrConsoleDivProxy implements IConsoleDivProxy { - keys: twrSharedCircularBuffer; - id:number; - - constructor(params:TConsoleDivProxyParams) { - const [className, id, keysBuffer] = params; - this.keys = new twrSharedCircularBuffer(keysBuffer); - this.id = id; - } - - charIn() { - return this.keys.readWait(); // wait for a key, then read it - } - - inkey() { - if (this.keys.isEmpty()) - return 0; - else - return this.charIn(); - } - - charOut(ch:number, codePoint:number) { - postMessage(["div-charout", [this.id, ch, codePoint]]); - } - - putStr(str:string):void - { - postMessage(["div-putstr", [this.id, str]]); - } - - getProp(propName: string) { - if (propName==="type") return IOTypes.CHARWRITE|IOTypes.CHARREAD; - console.log("twrConsoleDivProxy.getProp passed unknown property name: ", propName) - return 0; + twrConPutStr(callingMod:IWasmModule|IWasmModuleAsync, chars:number, codePage:number) { + this.putStr(callingMod.wasmMem.getString(chars, undefined, codePage)); } - setFocus() { - postMessage(["div-focus", [this.id]]); - } } +export default twrConsoleDiv; diff --git a/source/twr-ts/twrcondummy.ts b/source/twr-ts/twrcondummy.ts new file mode 100644 index 00000000..b166540c --- /dev/null +++ b/source/twr-ts/twrcondummy.ts @@ -0,0 +1,121 @@ +import {IConsoleStreamOut, IConsoleStreamIn, IConsoleCanvas, IConsoleAddressable, ICanvasProps } from "./twrcon.js" +import {IWasmModuleAsync} from "./twrmodasync.js"; +import {IWasmModule} from "./twrmod.js" +import {twrLibrary, TLibImports, twrLibraryInstanceRegistry} from "./twrlibrary.js"; + +// This class exists so the twrlibbuiltin can cause all functions (like twrConCls) to resolve at runtime link time +// twr.a links to all of these, even if the relevant console is not loaded by the app at runtime +// These functions should never be called, because twrLibrary routes a call (like io_cls(id)) to the correct console instance based on id +// see TODO comments in twrLibrary.ts for possible better fixes + +export default class twrConsoleDummy extends twrLibrary implements IConsoleStreamIn, IConsoleStreamOut, IConsoleAddressable, IConsoleCanvas { + id:number; + + imports:TLibImports = { + twrConCharOut:{noBlock:true}, + twrConGetProp:{}, + twrConPutStr:{noBlock:true}, + twrConCharIn:{isAsyncFunction: true, isModuleAsyncOnly: true}, + twrConSetFocus:{noBlock:true}, + twrConSetC32:{noBlock:true}, + twrConCls:{noBlock:true}, + twrConSetRange:{noBlock:true}, + twrConSetReset:{noBlock:true}, + twrConPoint:{}, + twrConSetCursor:{noBlock:true}, + twrConSetCursorXY:{noBlock:true}, + twrConSetColors:{noBlock:true}, + twrConDrawSeq:{}, + twrConLoadImage:{isModuleAsyncOnly:true, isAsyncFunction:true}, + }; + + libSourcePath = new URL(import.meta.url).pathname; + interfaceName = "twrConsole"; + + constructor() { + // all library constructors should start with these two lines + super(); + this.id=twrLibraryInstanceRegistry.register(this); + } + + twrConGetProp(callingMod:IWasmModule|IWasmModuleAsync, pn:number):number { + throw new Error("internal error"); + } + + keyDown(ev:KeyboardEvent) { + throw new Error("internal error"); + } + + twrConCharOut(callingMod:any, c:number, codePage:number) { + throw new Error("internal error"); + } + + twrConPutStr(callingMod:IWasmModule|IWasmModuleAsync, chars:number, codePage:number) { + throw new Error("internal error"); + } + + twrConSetC32(callingMod:any, location:number, c32:number) : void { + throw new Error("internal error"); + } + + twrConCls() { + throw new Error("internal error"); + } + + twrConSetRange(callingMod:IWasmModule|IWasmModuleAsync, chars:number, start:number, len:number) { + throw new Error("internal error"); + } + + setRangeJS(start:number, values:number[]) { + throw new Error("internal error"); + } + + twrConSetReset(callingMod:IWasmModule|IWasmModuleAsync, x:number, y:number, isset:boolean) : void { + throw new Error("internal error"); + } + + twrConPoint(callingMod:IWasmModule|IWasmModuleAsync, x:number, y:number) : boolean { + throw new Error("internal error"); + } + + twrConSetCursor(callingMod:IWasmModule|IWasmModuleAsync, location:number) : void { + throw new Error("internal error"); + } + + twrConSetCursorXY(callingMod:IWasmModule|IWasmModuleAsync, x:number, y:number) { + throw new Error("internal error"); + } + + twrConSetColors(callingMod:IWasmModule|IWasmModuleAsync, foreground:number, background:number) : void { + throw new Error("internal error"); + } + + async twrConCharIn_async(callingMod: IWasmModuleAsync):Promise { + throw new Error("internal error"); + } + + twrConSetFocus() { + throw new Error("internal error"); + } + + twrConDrawSeq(mod:IWasmModuleAsync|IWasmModule, ds:number) { + throw new Error("internal error"); + } + + getProp(name:keyof ICanvasProps): number { + throw new Error("internal error"); + } + + putStr(str:string) { + throw new Error("internal error"); + } + + charOut(c32:string) { + throw new Error("internal error"); + } + + twrConLoadImage_async(mod: IWasmModuleAsync, urlPtr: number, id: number) : Promise { + throw new Error("internal error"); + } + +} diff --git a/source/twr-ts/twrconreg.ts b/source/twr-ts/twrconreg.ts deleted file mode 100644 index 3219748e..00000000 --- a/source/twr-ts/twrconreg.ts +++ /dev/null @@ -1,60 +0,0 @@ - -import {IConsole, IConsoleProxy} from "./twrcon.js" - -// this is global in the JS main thread address space -// all consoles are registered here -export class twrConsoleRegistry { - - static consoles: IConsole[]=[]; - - // create a pairing between an instance of type IConsole and an integer ID - static registerConsole(con:IConsole) { - twrConsoleRegistry.consoles.push(con); - return twrConsoleRegistry.consoles.length-1; - } - - static getConsole(id:number) { - if (id<0 || id >= twrConsoleRegistry.consoles.length) - throw new Error("Invalid console ID: "+id); - - return twrConsoleRegistry.consoles[id]; - } - - static getConsoleID(con:IConsole) { - for (let i=0; i= twrConsoleProxyRegistry.consoles.length) - throw new Error("Invalid console ID: "+id); - - return twrConsoleProxyRegistry.consoles[id]; - } - - static getConsoleID(con:IConsoleProxy) { - for (let i=0; ivoid; + + imports:TLibImports = { + twrConCharOut:{noBlock:true}, + twrConGetProp:{}, + twrConPutStr:{noBlock:true}, + twrConCharIn:{isAsyncFunction: true, isModuleAsyncOnly: true}, + twrConSetFocus:{noBlock:true}, + twrConSetC32:{noBlock:true}, + twrConCls:{noBlock:true}, + twrConSetRange:{noBlock:true}, + twrConSetReset:{noBlock:true}, + twrConPoint:{}, + twrConSetCursor:{noBlock:true}, + twrConSetCursorXY:{noBlock:true}, + twrConSetColors:{noBlock:true}, + }; + + libSourcePath = new URL(import.meta.url).pathname; + interfaceName = "twrConsole"; constructor (canvasElement:HTMLCanvasElement, params:IConsoleTerminalParams={}) { + // all library constructors should start with these two lines + super(); + this.id=twrLibraryInstanceRegistry.register(this); const {foreColor="white", backColor="black", fontSize=16, widthInChars=80, heightInChars=25} = params; @@ -101,128 +123,31 @@ export class twrConsoleTerminal implements IConsoleTerminal { this.cellH2 = this.cellH1; this.cellH3 = this.cellHeight - this.cellH1 - this.cellH2; - this.cls(); + this.twrConCls(); this.cpTranslate=new twrCodePageToUnicodeCodePoint(); - - this.id=twrConsoleRegistry.registerConsole(this); - } - // ProxyParams are used as the constructor options to create the Proxy class as returned by getProxyClassName, - // in the twrModAsyncProxy WebWorker thread - getProxyParams() : TConsoleTerminalProxyParams { - if (this.returnValue || this.keys) throw new Error("internal error -- getProxyParams unexpectedly called twice."); - // these are used to communicate with twrConsoleTerminalProxy (if it exists) - // tsconfig, lib must be set to 2017 or higher for SharedArrayBuffer usage - this.returnValue = new twrSharedCircularBuffer(); - this.keys = new twrSharedCircularBuffer(); - return ["twrConsoleTerminalProxy", this.id, this.returnValue.sharedArray, this.keys.sharedArray]; - } - getProp(propName: string): number { return this.props[propName]; } + + twrConGetProp(callingMod:IWasmModule|IWasmModuleAsync, pn:number):number { + const propName=callingMod.wasmMem.getString(pn); + return this.getProp(propName); + } - keyDown(ev:KeyboardEvent) { - keyDownUtil(this, ev); - } - - // these messages are sent by twrConsoleTerminalProxy to cause functions to execute in the JS Main Thread - processMessage(msgType:string, data:[number, ...any[]]):boolean { - const [id, ...params] = data; - if (id!=this.id) throw new Error("internal error"); // should never happen - - switch (msgType) { - case "term-getprop": - const [propName] = params; - const propVal=this.getProp(propName); - this.returnValue!.write(propVal); - break; - - case "term-point": - { - const [x, y] = params; - const r=this.point(x, y); - this.returnValue!.write(r?1:0); // wait for result, then read it - - } - break; - - case "term-charout": - { - const [ch, codePage] = params; - this.charOut(ch, codePage); - } - break; - - case "term-putstr": - { - const [str] = params; - this.putStr(str); - } - break; - - case "term-cls": - { - this.cls(); - } - break; - - case "term-setrange": - { - const [start, values] = params; - this.setRange(start, values); - } - break; - - case "term-setc32": - { - const [location, char] = params; - this.setC32(location, char); - } - break; - - case "term-setreset": - { - const [x, y, isset] = params; - this.setReset(x, y, isset); - } - break; - - case "term-setcursor": - { - const [pos] = params; - this.setCursor(pos); - } - break; - - case "term-setcursorxy": - { - const [x, y] = params; - this.setCursorXY(x, y); - } - break; - - case "term-setcolors": - { - const [foreground, background] = params; - this.setColors(foreground, background); + keyDown(ev:KeyboardEvent) { + if (this.keyWaiting) { + const r=keyEventToCodePoint(ev); + if (r) { + this.keyWaiting(r); + this.keyWaiting=undefined; } - break; - - case "term-focus": - { - this.element.focus(); - } - break; - - - default: - return false; } - - return true; + else { + this.keyBuffer.push(ev); + } } private RGB_TO_RGBA(x:number) { @@ -232,15 +157,27 @@ export class twrConsoleTerminal implements IConsoleTerminal { private eraseLine() { for (let i=this.props.cursorPos; i < Math.floor(this.props.cursorPos/this.props.widthInChars)*this.props.widthInChars+this.props.widthInChars; i++) - this.setC32(i, 32); + this.setC32(i, " "); } - charOut(c:number, codePage:number) + twrConCharOut(callingMod:any, c:number, codePage:number) { - if (c==13 || c==10) // return + + const c32=this.cpTranslate.convert(c, codePage); + if (c32===0) return; + + this.charOut(String.fromCodePoint(c32)); + } + + charOut(c32:string) { + + if (c32.length>1) + throw new Error("charOut takes an empty or single char string"); + + if (c32==="\n") // newline { if (this.isCursorVisible) - this.setC32(this.props.cursorPos,32); + this.setC32(this.props.cursorPos, " "); this.props.cursorPos = Math.floor(this.props.cursorPos/this.props.widthInChars); this.props.cursorPos = this.props.cursorPos*this.props.widthInChars; @@ -250,69 +187,66 @@ export class twrConsoleTerminal implements IConsoleTerminal { if (this.props.cursorPos < this.size) this.eraseLine(); } - else if (c==8) // backspace + else if (c32==="\x08") // backspace { if (this.props.cursorPos > 0) { if (this.isCursorVisible) - this.setC32(this.props.cursorPos,32); + this.setC32(this.props.cursorPos, " "); this.props.cursorPos--; - this.setC32(this.props.cursorPos,32); + this.setC32(this.props.cursorPos, " "); } } - else if (c==0xE) // Turn on cursor + else if (c32==="\x0E") // Turn on cursor, TRS-80 CODE, should probably update to ANSI { this.isCursorVisible = true; } - else if (c==0xF) // Turn off cursor + else if (c32==="\x0F") // Turn off cursor, TRS-80 code { - this.setC32(this.props.cursorPos,32); + this.setC32(this.props.cursorPos, " "); this.isCursorVisible = false; } - else if (c==24) /* backspace cursor*/ + else if (c32===String.fromCharCode(24)) /* backspace cursor*/ { if (this.props.cursorPos > 0) this.props.cursorPos--; } - else if (c==25) /* advance cursor*/ + else if (c32===String.fromCharCode(25)) /* advance cursor*/ { if (this.props.cursorPos < (this.size-1)) this.props.cursorPos++; } - else if (c==26) /* cursor down one line */ + else if (c32===String.fromCharCode(26)) /* cursor down one line */ { if (this.props.cursorPos < this.props.widthInChars*(this.props.heightInChars-1)) this.props.cursorPos+=this.props.widthInChars; } - else if (c==27) /* cursor up one line */ + else if (c32===String.fromCharCode(27)) /* cursor up one line */ { if (this.props.cursorPos >= this.props.widthInChars) this.props.cursorPos-=this.props.widthInChars; } - else if (c==28) /* home */ + else if (c32===String.fromCharCode(28)) /* home */ { this.props.cursorPos=0; } - else if (c==29) /* beginning of line */ + else if (c32===String.fromCharCode(29)) /* beginning of line */ { this.props.cursorPos=(this.props.cursorPos/this.props.widthInChars)*this.props.widthInChars; } - else if (c==30) /* erase to end of line */ + else if (c32===String.fromCharCode(30)) /* erase to end of line */ { this.eraseLine(); } - else if (c==31) /* erase to end of frame */ + else if (c32===String.fromCharCode(31)) /* erase to end of frame */ { for (let i=this.props.cursorPos; i < this.size; i++) - this.setC32(i, 32); + this.setC32(i, " "); } else { - const c32=this.cpTranslate.convert(c, codePage); - if (c32!=0) { - this.setC32(this.props.cursorPos, c32); - this.props.cursorPos++; - } + this.setC32(this.props.cursorPos, c32); + this.props.cursorPos++; } // Do we need to scroll? @@ -335,11 +269,11 @@ export class twrConsoleTerminal implements IConsoleTerminal { } if (this.isCursorVisible) - this.setC32(this.props.cursorPos, 9611); // 9611 is graphic block -- same cursor i use in class twrDiv + this.setC32(this.props.cursorPos, String.fromCodePoint(9611)); // 9611 is graphic block -- same cursor i use in class twrDiv if (this.props.cursorPos >= this.size) { - throw new Error("twrTerm: assert: this.props.cursorPos >= this.size"); + throw new Error("internal error: this.props.cursorPos >= this.size"); } } @@ -347,14 +281,27 @@ export class twrConsoleTerminal implements IConsoleTerminal { putStr(str:string) { for (let i=0; i < str.length; i++) - this.charOut(str.codePointAt(i)||0, codePageUTF32); + this.twrConCharOut(undefined, str.codePointAt(i)||0, codePageUTF32); + } + + twrConPutStr(callingMod:IWasmModule|IWasmModuleAsync, chars:number, codePage:number) { + const str=callingMod.wasmMem.getString(chars, undefined, codePage); + for (let i=0; i < str.length; i++) + this.twrConCharOut(callingMod, str.codePointAt(i)||0, codePageUTF32); } //************************************************* + setC32(location:number, str:string) : void { + if (str.length>1) + throw new Error("setC32 takes an empty or single char string"); - setC32(location:number, c32:number) : void + this.twrConSetC32(undefined, location, str.codePointAt(0)||0) + } + + twrConSetC32(callingMod:any, location:number, c32:number) : void { - if (!(location>=0 && location=0 && location=this.size) throw new Error("setCursor: invalid location: "+location); @@ -555,96 +509,47 @@ export class twrConsoleTerminal implements IConsoleTerminal { //************************************************* - setCursorXY(x:number, y:number) { + twrConSetCursorXY(callingMod:IWasmModule|IWasmModuleAsync, x:number, y:number) { if (x<0 || y<0 || this.props.widthInChars*y+x >= this.size) throw new Error("setCursorXY: invalid parameter(s)"); - this.setCursor(this.props.widthInChars*y+x); + this.twrConSetCursor(callingMod, this.props.widthInChars*y+x); } //************************************************* - setColors(foreground:number, background:number) : void + twrConSetColors(callingMod:IWasmModule|IWasmModuleAsync, foreground:number, background:number) : void { this.props.foreColorAsRGB=foreground; this.props.backColorAsRGB=background; } -} - -//************************************************* - -export class twrConsoleTerminalProxy implements IConsoleTerminalProxy { - keys: twrSharedCircularBuffer; - returnValue: twrSharedCircularBuffer; - id:number; - - constructor(params:TConsoleTerminalProxyParams) { - const [className, id, returnBuffer, keysBuffer] = params; - this.keys = new twrSharedCircularBuffer(keysBuffer); - this.returnValue = new twrSharedCircularBuffer(returnBuffer); - this.id=id; - } - getProp(propName: string):number - { - postMessage(["term-getprop", [this.id, propName]]); - return this.returnValue.readWait(); // wait for result, then read it - } - - charIn() { - return this.keys.readWait(); // wait for a key, then read it - } - - point(x:number, y:number):boolean - { - postMessage(["term-point", [this.id, x, y]]); - return this.returnValue.readWait()!=0; // wait for result, then read it - } - - charOut(ch:number, codePoint:number) { - postMessage(["term-charout", [this.id, ch, codePoint]]); - } - - putStr(str:string):void - { - postMessage(["term-putstr", [this.id, str]]); - } - - cls():void - { - postMessage(["term-cls", [this.id]]); - } - - setRange(start:number, values:[]):void - { - postMessage(["term-setrange", [this.id, start, values]]); - } + // TODO!! Should keyBuffer be flushed? Is keyBuffer needed? + async twrConCharIn_async(callingMod: IWasmModuleAsync):Promise { + let ev:KeyboardEvent|undefined; - setC32(location:number, char:number):void - { - postMessage(["term-setc32", [this.id, location, char]]); - } + return new Promise( (resolve) => { + if (this.keyWaiting) + throw new Error("internal error"); + while (ev=this.keyBuffer.shift()) { + const r=keyEventToCodePoint(ev); + if (r) { + resolve(r); + return; + } + } - setReset(x:number, y:number, isset:boolean):void - { - postMessage(["term-setreset", [this.id, x, y, isset]]); - } + this.keyWaiting=resolve; - setCursor(pos:number):void - { - postMessage(["term-setcursor", [this.id, pos]]); + }); } - setCursorXY(x:number, y:number):void - { - postMessage(["term-setcursorxy", [this.id, x, y]]); + twrConSetFocus() { + this.element.focus(); } +} - setColors(foreground:number, background:number):void - { - postMessage(["term-setcolors", [this.id, foreground, background]]); - } +export default twrConsoleTerminal; + +//TODO!! Most of these member functions could benefit from a FireAndForget option - setFocus() { - postMessage(["term-focus", [this.id]]); - } -} \ No newline at end of file + \ No newline at end of file diff --git a/source/twr-ts/twrdate.ts b/source/twr-ts/twrdate.ts deleted file mode 100644 index ffcd8733..00000000 --- a/source/twr-ts/twrdate.ts +++ /dev/null @@ -1,7 +0,0 @@ -// return ms since epoch as double -export function twrTimeEpochImpl() { - - return Date.now(); - -} - diff --git a/source/twr-ts/twreventqueue.ts b/source/twr-ts/twreventqueue.ts new file mode 100644 index 00000000..d97f80bd --- /dev/null +++ b/source/twr-ts/twreventqueue.ts @@ -0,0 +1,157 @@ +import {twrSharedCircularBuffer} from "./twrcircular.js" +import {twrWasmModuleAsyncProxy} from "./twrmodasyncproxy.js"; + +const eventMarker=0x684610d6; // random positive 32 bit value +const mallocMarker=0x51949385; // random positive 32 bit value + +export class twrEventQueueSend { + circBuffer: twrSharedCircularBuffer=new twrSharedCircularBuffer(); + +//TOOD!! unify / rename TOnEventCallback = (eventID:number, ...args:number[])=>void; + postEvent(eventID:number, ...params:number[]):void { + this.circBuffer.writeArray([eventMarker, eventID, params.length, ...params]); + } + + postMalloc(mallocID:number, size:number) { + this.circBuffer.writeArray([mallocMarker, mallocID, size]); + } +} + +export type TOnEventCallback = (eventID:number, ...args:number[])=>void; + + +export class twrEventQueueReceive { + circBuffer: twrSharedCircularBuffer; + pendingEventIDs: number[]; + pendingEventArgs: (number[])[]; + ownerMod:twrWasmModuleAsyncProxy; + static unqiueInt:number=1; + static onEventCallbacks:(TOnEventCallback|undefined)[]=[]; + + constructor(ownerMod:twrWasmModuleAsyncProxy, eventQueueBuffer:SharedArrayBuffer) { + this.circBuffer=new twrSharedCircularBuffer(eventQueueBuffer); + this.pendingEventIDs=[]; + this.pendingEventArgs=[]; + this.ownerMod=ownerMod; + } + + private readEventRemainder() { + const eventID=this.circBuffer.read(); + if (eventID===undefined) throw new Error ("internal error"); + const argLen=this.circBuffer.read(); + if (argLen===undefined) throw new Error ("internal error"); + const args:number[]=[]; + for (let i=0; i < argLen; i++) { + const arg=this.circBuffer.read(); + if (arg===undefined) throw new Error ("internal error"); + args.push(arg); + } + + if (!(eventID in twrEventQueueReceive.onEventCallbacks)) + throw new Error("internal error"); + + this.pendingEventIDs.push(eventID); + this.pendingEventArgs.push(args); + } + + private readMallocRemainder() { + const mallocID=this.circBuffer.read(); + if (mallocID===undefined) throw new Error ("internal error"); + const size=this.circBuffer.read(); + if (size===undefined) throw new Error ("internal error"); + const ptr=this.ownerMod.wasmMem.malloc(size); + postMessage(["twrWasmModule", mallocID, "callCOkay", ptr]); // we are in the twrWasmModuleAsyncProxy main thread + } + + private readCommandRemainder(firstValue:number|undefined) { + if (firstValue===eventMarker) + this.readEventRemainder(); + else if (firstValue===mallocMarker) + this.readMallocRemainder(); + else + throw new Error ("internal error -- eventMarker or mallocMarker expected but not found"); + } + + // called only if circBuffer not empty + private readCommand() { + const firstValue=this.circBuffer.read(); + this.readCommandRemainder(firstValue); + } + + private readWaitCommand() { + const firstValue=this.circBuffer.readWait(); + this.readCommandRemainder(firstValue); + } + + private findEvent(filterEvent:number) : [undefined | number, undefined | number[], undefined | number] { + + if (filterEvent===undefined) { + return [this.pendingEventIDs.shift(), this.pendingEventArgs.shift(), 0] + } + + const index=this.pendingEventIDs.indexOf(filterEvent); + if (index!=-1) + return [this.pendingEventIDs.splice(index, 1)[0], this.pendingEventArgs.splice(index, 1)[0], index]; + + return [undefined, undefined, undefined]; + } + + + waitEvent(filterEvent:number) : [eventID:number, args:number[]] { + while (true) { + // empty the queue + while (!this.circBuffer.isEmpty()) + this.readCommand(); + + // is our event in the queue? + const [eventID, args, index]=this.findEvent(filterEvent); + // execute callbacks up to this filterEvent (so as to call them in order) + // if filterEvent not found, index is undefined, which causes doCallbacks to execute all pendingEventIDs + // this call commented out so that the C events act like JavaScript events/callbacks (only called when main function finishes) + // to consider: allow callbacks in sync blocking functions like sleep (that use await in their implementations) + //this.doCallbacks(index); + if (eventID && args) { + return [eventID, args]; + } + + // wait for a new event + this.readWaitCommand(); + } + } + + private doCallbacks(upToIndex?:number) { + const end=upToIndex?upToIndex:this.pendingEventIDs.length; + console.log("end",end, upToIndex, this.pendingEventIDs.length); + for (let i=0; imax_precision) - r=value.toPrecision(max_precision); - this.mod.copyString(buffer, buffer_size, r); - } - } - - toFixed(buffer:number, buffer_size:number, value:number, decdigits:number):void { - const r=value.toFixed(decdigits); - this.mod.copyString(buffer, buffer_size, r); - } - - toExponential(buffer:number, buffer_size:number, value:number, decdigits:number):void { - const r=value.toExponential(decdigits); - this.mod.copyString(buffer, buffer_size, r); - } - - // emulates the MS C lib function _fcvt_s, but doesn't support all ranges of number. - // Number.toFixed() has a max size of 100 fractional digits, and values must be less than 1e+21 - // Negative exponents must be now smaller than 1e-99 - // fully-function C version also int he source, but this is the version enabled by default - fcvtS( - buffer:number, // char * - sizeInBytes:number, //size_t - value:number, // double - fracpart_numdigits:number, //int - dec:number, // int * - sign:number // int * - ):number { - - if (buffer==0 ||sign==0 || dec==0 || sizeInBytes<1) return 1; - - let digits:string; - let decpos:number; - let s=0; // default to positive - - - if (Number.isNaN(value)) { /* nan */ - digits="1#QNAN00000000000000000000000000000".slice(0, fracpart_numdigits+1); - decpos=1; - } - else if (!Number.isFinite(value)) { /* infinity */ - digits="1#INF00000000000000000000000000000".slice(0, fracpart_numdigits+1); - decpos=1; - } - else if (value==0) { - digits="000000000000000000000000000000000000".slice(0,fracpart_numdigits); - decpos=0; - } - - else { - - if (value<0) { - s=1; // negative - value=Math.abs(value); - } - - if (fracpart_numdigits>100 || value > 1e+21 || value < 1e-99) { - this.mod.copyString(buffer, sizeInBytes, ""); - this.mod.mem32[dec]=0; - return 1; - } - - const roundValStr=value.toFixed(fracpart_numdigits); - let [intPart="", fracPart=""] = roundValStr.split('.'); - if (intPart=="0") intPart=""; - - if (intPart.length>0) { // has integer part - decpos=intPart.length; - digits=intPart+fracPart; - } - else { // only a fraction - digits=fracPart.replace(/^0+/,""); // remove leading zeros - decpos=digits.length-fracPart.length; - } - } - - if (sizeInBytes-1 < digits.length) return 1; - this.mod.copyString(buffer, sizeInBytes, digits); - this.mod.setLong(dec, decpos); - this.mod.setLong(sign, s); - - return 0; - - /* - this version 'works' with larger numbers than using toFixed, but doesn't round correctly - - let decpos=0; - let digits:string; - if (value!=0) decpos=Math.floor(Math.log10(value))+1; - - if (decpos>0) { // has integer part - const intlen=Math.max(decpos, 0); - console.log("intlen=",intlen, "decpos=",decpos); - const [nonExponent, exponent=0] = value.toPrecision(intlen+fracpart_numdigits).split('e'); - digits=nonExponent.replace('.', ''); - digits=digits.replace(/^0+/,""); // remove leading zeros - } - else { // only a fraction - const intpart=Math.trunc(value); - const fracpart=value-intpart; - const prec=fracpart_numdigits- (-decpos); - console.log("prec=",prec); - if (prec<1) { - digits=""; - } - else { - const [nonExponent, exponent=0] = fracpart.toPrecision(prec).split('e'); - digits=nonExponent.replace('.', ''); - digits=digits.replace(/^0+/,""); - } - } - - console.log("fcvtS value",value,"fracpart_numdigits",fracpart_numdigits); - console.log('digits=',digits); - console.log('dec=',decpos); - console.log("sign=",s); - */ - - - } -} \ No newline at end of file diff --git a/source/twr-ts/twrlibaudio.ts b/source/twr-ts/twrlibaudio.ts new file mode 100644 index 00000000..03e84b14 --- /dev/null +++ b/source/twr-ts/twrlibaudio.ts @@ -0,0 +1,592 @@ +import { TLibImports, twrLibrary, twrLibraryInstanceRegistry } from "./twrlibrary.js"; +import { IWasmModule } from "./twrmod"; +import { IWasmModuleAsync } from "./twrmodasync"; + +enum NodeType { + AudioBuffer, + HTMLAudioElement, +} +type Node = [NodeType.AudioBuffer, AudioBuffer]; + +type BufferPlaybackNode = [NodeType.AudioBuffer, AudioBufferSourceNode, number, number, GainNode, StereoPannerNode]; +type AudioPlaybackNode = [NodeType.HTMLAudioElement, HTMLAudioElement]; +type PlaybackNode = BufferPlaybackNode | AudioPlaybackNode; + +// enum AudioFileTypes { +// RAW, +// MP3, +// WAV, +// OGG, +// Unknown +// }; + +export default class twrLibAudio extends twrLibrary { + id: number; + + imports: TLibImports = { + "twrAudioFromFloatPCM": {}, + "twrAudioFrom8bitPCM": {}, + "twrAudioFrom16bitPCM": {}, + "twrAudioFrom32bitPCM": {}, + + "twrAudioGetFloatPCM": {isAsyncFunction: true}, + "twrAudioGet8bitPCM": {isAsyncFunction: true}, + "twrAudioGet16bitPCM": {isAsyncFunction: true}, + "twrAudioGet32bitPCM": {isAsyncFunction: true}, + + "twrAudioPlay": {}, + "twrAudioPlayRange": {}, + "twrAudioQueryPlaybackPosition": {}, + "twrAudioLoadSync": {isAsyncFunction: true, isModuleAsyncOnly: true}, + "twrAudioLoad": {}, + "twrAudioFreeID": {}, + "twrAudioStopPlayback": {}, + "twrAudioGetMetadata": {}, + "twrAudioPlaySync": {isAsyncFunction: true, isModuleAsyncOnly: true}, + "twrAudioRangePlaySync": {isAsyncFunction: true, isModuleAsyncOnly: true}, + "twrAudioModifyPlaybackVolume": {}, + "twrAudioModifyPlaybackPan": {}, + "twrAudioModifyPlaybackRate": {}, + "twrAudioPlayFile": {}, + }; + nextID: number = 0; + nextPlaybackID: number = 0; + context: AudioContext = new AudioContext(); + nodes: Node[] = []; + playbacks: PlaybackNode[] = []; + + + // every library should have this line + libSourcePath = new URL(import.meta.url).pathname; + + constructor() { + // all library constructors should start with these two lines + super(); + this.id=twrLibraryInstanceRegistry.register(this); + } + + + //loads audio from samples using createBuffer and then loading the given data in + //data is expected to be a 2d array of channels with data length equal to singleChannelDataLen + //so if len is 10 and channels is 1, data is of total length 10 + //if len is 10 and channels is 2, data is of total length 20, etc. + internalSetupAudioBuffer(numChannels: number, sampleRate: number, singleChannelDataLen: number): [AudioBuffer, number] { + const arrayBuffer = this.context.createBuffer( + numChannels, + singleChannelDataLen, + sampleRate + ); + + const id = this.nextID++; + this.nodes[id] = [NodeType.AudioBuffer, arrayBuffer]; + + return [arrayBuffer, id]; + } + + twrAudioFromFloatPCM(mod: IWasmModuleAsync|IWasmModule, numChannels: number, sampleRate: number, dataPtr: number, singleChannelDataLen: number) { + const [arrayBuffer, id] = this.internalSetupAudioBuffer(numChannels, sampleRate, singleChannelDataLen); + + for (let channel = 0; channel < numChannels; channel++) { + const channelBuff = arrayBuffer.getChannelData(channel); + const startPos = dataPtr/4.0 + channel*singleChannelDataLen; + channelBuff.set(mod.wasmMem.memF!.slice(startPos, startPos + singleChannelDataLen)); + } + + return id; + } + + twrAudioFrom8bitPCM(mod: IWasmModuleAsync|IWasmModule, numChannels: number, sampleRate: number, dataPtr: number, singleChannelDataLen: number) { + const [arrayBuffer, id] = this.internalSetupAudioBuffer(numChannels, sampleRate, singleChannelDataLen); + + for (let channel = 0; channel < numChannels; channel++) { + const channelBuff = arrayBuffer.getChannelData(channel); + const startPos = dataPtr/1.0 + channel*singleChannelDataLen; + + const dataBuff = mod.wasmMem.mem8.slice(startPos, startPos + singleChannelDataLen); + + for (let i = 0; i < singleChannelDataLen; i++) { + //convert 8-bit PCM to float + //data is signed, so it will also need to find the negatives + channelBuff[i] = dataBuff[i] > 127 ? (dataBuff[i] - 256)/128 : dataBuff[i]/128; + } + } + + return id; + } + + twrAudioFrom16bitPCM(mod: IWasmModuleAsync|IWasmModule, numChannels: number, sampleRate: number, dataPtr: number, singleChannelDataLen: number) { + const [arrayBuffer, id] = this.internalSetupAudioBuffer(numChannels, sampleRate, singleChannelDataLen); + + for (let channel = 0; channel < numChannels; channel++) { + const channelBuff = arrayBuffer.getChannelData(channel); + const startPos = dataPtr/2.0 + channel*singleChannelDataLen; + + const dataBuff = mod.wasmMem.mem16.slice(startPos, startPos + singleChannelDataLen); + + for (let i = 0; i < singleChannelDataLen*2; i += 2) { + //convert 16-bit PCM to float + channelBuff[i] = dataBuff[i] > 32767 ? (dataBuff[i] - 65536)/32768 : dataBuff[i]/32768; + } + } + + return id; + } + + twrAudioFrom32bitPCM(mod: IWasmModuleAsync|IWasmModule, numChannels: number, sampleRate: number, dataPtr: number, singleChannelDataLen: number) { + const [arrayBuffer, id] = this.internalSetupAudioBuffer(numChannels, sampleRate, singleChannelDataLen); + + for (let channel = 0; channel < numChannels; channel++) { + const channelBuff = arrayBuffer.getChannelData(channel); + const startPos = dataPtr/4.0 + channel*singleChannelDataLen; + + const dataBuff = mod.wasmMem.mem32.slice(startPos, startPos + singleChannelDataLen); + + for (let i = 0; i < singleChannelDataLen; i++) { + //convert 32-bit PCM to float + channelBuff[i] = dataBuff[i] > 2147483647 ? (dataBuff[i] - 4294967296)/2147483648 : dataBuff[i]/2147483648; + } + } + + return id; + } + internalGetAnyPCMPart1(mod: IWasmModuleAsync|IWasmModule, nodeID: number, singleChannelDataLenPtr: number, channelPtr: number): [AudioBuffer, number] { + if (!(nodeID in this.nodes)) throw new Error(`twrAudioGetSamples couldn't find node of ID ${nodeID}`); + + const node = this.nodes[nodeID]; + if (node[0] != NodeType.AudioBuffer) throw new Error(`twrAudioGetSamples expected a node of type AudioBuffer, got ${NodeType[node[0]]}!`); + + const audioBuffer = node[1] as AudioBuffer; + + const totalLen = audioBuffer.length * audioBuffer.numberOfChannels; + + mod.wasmMem.setLong(singleChannelDataLenPtr, audioBuffer.length); + mod.wasmMem.setLong(channelPtr, audioBuffer.numberOfChannels); + + return [audioBuffer, totalLen]; + } + + internalSyncGetAnyPCM(mod: IWasmModule, nodeID: number, singleChannelDataLenPtr: number, channelPtr: number, dataSize: number, part2: (a: IWasmModuleAsync|IWasmModule, b: AudioBuffer, c: number) => void) { + const [node, totalLen] = this.internalGetSamplesPart1(mod, nodeID, singleChannelDataLenPtr, channelPtr); + const bufferPtr = mod.malloc!(totalLen * dataSize); //len(floatArray) * dataSize bytes/item + part2(mod, node, bufferPtr); + + return bufferPtr + } + + async internalAsyncGetAnyPCM(mod: IWasmModuleAsync, nodeID: number, singleChannelDataLenPtr: number, channelPtr: number, dataSize: number, part2: (a: IWasmModuleAsync|IWasmModule, b: AudioBuffer, c: number) => void) { + const [node, totalLen] = this.internalGetSamplesPart1(mod, nodeID, singleChannelDataLenPtr, channelPtr); + const bufferPtr = await mod.malloc!(totalLen * dataSize); //len(floatArray) * dataSize bytes/item + part2(mod, node, bufferPtr); + + return bufferPtr + } + + internalGetSamplesPart1(mod: IWasmModuleAsync|IWasmModule, nodeID: number, singleChannelDataLenPtr: number, channelPtr: number): [AudioBuffer, number] { + if (!(nodeID in this.nodes)) throw new Error(`twrAudioGetSamples couldn't find node of ID ${nodeID}`); + + const node = this.nodes[nodeID]; + if (node[0] != NodeType.AudioBuffer) throw new Error(`twrAudioGetSamples expected a node of type AudioBuffer, got ${NodeType[node[0]]}!`); + + const audioBuffer = node[1] as AudioBuffer; + + const totalLen = audioBuffer.length * audioBuffer.numberOfChannels; + + mod.wasmMem.setLong(singleChannelDataLenPtr, audioBuffer.length); + mod.wasmMem.setLong(channelPtr, audioBuffer.numberOfChannels); + + return [audioBuffer, totalLen]; + } + internalGetFloatPCMPart2(mod: IWasmModuleAsync|IWasmModule, buffer: AudioBuffer, bufferPtr: number) { + for (let channel = 0; channel < buffer.numberOfChannels; channel++) { + let data = buffer.getChannelData(channel); + const startPos = bufferPtr/4 + channel*buffer.length; + mod.wasmMem.memF.set(data.slice(0, buffer.length), startPos); + } + } + + //Separated into a sync and async module, gets the total amount of data stored + //mallocs a buffer of appropriate size (split by sync and async since async needs await) + //then copies the audio buffer to the malloced memory and returns the pointer to the memory + twrAudioGetFloatPCM(mod: IWasmModule, nodeID: number, singleChannelDataLenPtr: number, channelPtr: number) { + return this.internalSyncGetAnyPCM(mod, nodeID, singleChannelDataLenPtr, channelPtr, 4, this.internalGetFloatPCMPart2); + } + + async twrAudioGetFloatPCM_async(mod: IWasmModuleAsync, nodeID: number, singleChannelDataLenPtr: number, channelPtr: number) { + return await this.internalAsyncGetAnyPCM(mod, nodeID, singleChannelDataLenPtr, channelPtr, 4, this.internalGetFloatPCMPart2); + } + + internalGet8bitPCMPart2(mod: IWasmModuleAsync|IWasmModule, buffer: AudioBuffer, bufferPtr: number) { + for (let channel = 0; channel < buffer.numberOfChannels; channel++) { + let data = buffer.getChannelData(channel); + const startPos = bufferPtr + channel*buffer.length; + const retBuffer = mod.wasmMem.mem8.slice(startPos, buffer.length); + + for (let i = 0; i < buffer.length; i++) { + //nergative values will automatically be converted to unsigned when assigning to retBuffer + retBuffer[i] = Math.round(data[i] * 128); + } + } + } + + twrAudioGet8bitPCM(mod: IWasmModule, nodeID: number, singleChannelDataLenPtr: number, channelPtr: number) { + return this.internalSyncGetAnyPCM(mod, nodeID, singleChannelDataLenPtr, channelPtr, 1, this.internalGet8bitPCMPart2); + } + + async twrAudioGet8bitPCM_async(mod: IWasmModuleAsync, nodeID: number, singleChannelDataLenPtr: number, channelPtr: number) { + return await this.internalAsyncGetAnyPCM(mod, nodeID, singleChannelDataLenPtr, channelPtr, 1, this.internalGet8bitPCMPart2); + } + + internalGet16bitPCMPart2(mod: IWasmModuleAsync|IWasmModule, buffer: AudioBuffer, bufferPtr: number) { + for (let channel = 0; channel < buffer.numberOfChannels; channel++) { + let data = buffer.getChannelData(channel); + const startPos = bufferPtr/2.0 + channel*buffer.length; + const retBuffer = mod.wasmMem.mem16.slice(startPos, buffer.length); + + for (let i = 0; i < buffer.length; i++) { + //nergative values will automatically be converted to unsigned when assigning to retBuffer + retBuffer[i] = Math.round(data[i] * 32768); + } + } + } + + twrAudioGet16bitPCM(mod: IWasmModule, nodeID: number, singleChannelDataLenPtr: number, channelPtr: number) { + return this.internalSyncGetAnyPCM(mod, nodeID, singleChannelDataLenPtr, channelPtr, 2, this.internalGet16bitPCMPart2); + } + + async twrAudioGet16bitPCM_async(mod: IWasmModuleAsync, nodeID: number, singleChannelDataLenPtr: number, channelPtr: number) { + return await this.internalAsyncGetAnyPCM(mod, nodeID, singleChannelDataLenPtr, channelPtr, 2, this.internalGet16bitPCMPart2); + } + + internalGet32bitPCMPart2(mod: IWasmModuleAsync|IWasmModule, buffer: AudioBuffer, bufferPtr: number) { + for (let channel = 0; channel < buffer.numberOfChannels; channel++) { + let data = buffer.getChannelData(channel); + const startPos = bufferPtr/4.0 + channel*buffer.length; + const retBuffer = mod.wasmMem.mem32.slice(startPos, buffer.length); + + for (let i = 0; i < buffer.length; i++) { + //nergative values will automatically be converted to unsigned when assigning to retBuffer + retBuffer[i] = Math.round(data[i] * 2147483648); + } + } + } + + twrAudioGet32bitPCM(mod: IWasmModule, nodeID: number, singleChannelDataLenPtr: number, channelPtr: number) { + return this.internalSyncGetAnyPCM(mod, nodeID, singleChannelDataLenPtr, channelPtr, 4, this.internalGet32bitPCMPart2); + } + + async twrAudioGet21bitPCM_async(mod: IWasmModuleAsync, nodeID: number, singleChannelDataLenPtr: number, channelPtr: number) { + return await this.internalAsyncGetAnyPCM(mod, nodeID, singleChannelDataLenPtr, channelPtr, 4, this.internalGet32bitPCMPart2); + } + + + //starts playing an audio node, + //all nodes are cloned by default so they can be played multiple times + //therefor, a new playback_id is returned for querying status + twrAudioPlay(mod: IWasmModuleAsync|IWasmModule, nodeID: number, volume: number = 1, pan: number = 0, finishCallback: number | null = null) { + return this.twrAudioPlayRange(mod, nodeID, 0, null, false, null, volume, pan, finishCallback); + } + + internalAudioPlayRange(mod: IWasmModuleAsync|IWasmModule, nodeID: number, startSample: number, endSample: number | null, loop: boolean, sampleRate: number | null, volume: number, pan: number): [Promise, number] { + if (!(nodeID in this.nodes)) throw new Error(`twrLibAudio twrAudioPlayNode was given a non-existant nodeID (${nodeID})!`); + + if (sampleRate == 0) { //assume a 0 sample_rate is just normal speed or null + sampleRate = null; + } + + const node = this.nodes[nodeID]; + + let id = this.nextPlaybackID++; + let promise: Promise; + + switch (node[0]) { + case NodeType.AudioBuffer: + { + if (endSample == null) { + endSample = node[1].length; + } + + const buffer = node[1] as AudioBuffer; + const sourceBuffer = this.context.createBufferSource(); + sourceBuffer.buffer = buffer; + + promise = new Promise((resolve, reject) => { + sourceBuffer.onended = () => { + delete this.playbacks[id]; + resolve(id); + } + }); + + + const startTime = startSample/node[1].sampleRate; + const endTime = (endSample - startSample)/node[1].sampleRate; + + sourceBuffer.loop = loop; + sourceBuffer.loopStart = startTime; + sourceBuffer.loopEnd = endSample/node[1].sampleRate; + + sourceBuffer.playbackRate.value = sampleRate ? sampleRate/node[1].sampleRate : 1.0; + + sourceBuffer.start(0, startTime, loop ? undefined : endTime); + + const gainNode = this.context.createGain(); + gainNode.gain.value = volume; + sourceBuffer.connect(gainNode); + + const panNode = this.context.createStereoPanner(); + panNode.pan.value = pan; + gainNode.connect(panNode); + panNode.connect(this.context.destination); + + this.playbacks[id] = [NodeType.AudioBuffer, sourceBuffer, (new Date()).getTime(), node[1].sampleRate, gainNode, panNode]; + } + break; + + default: + throw new Error(`twrAudioPlayNode unknown type! ${node[0]}`); + } + + return [promise, id]; + } + + twrAudioPlayRange(mod: IWasmModuleAsync|IWasmModule, nodeID: number, startSample: number, endSample: number | null = null, loop: boolean = false, sampleRate: number | null = null, volume: number = 1, pan: number = 0, finishCallback: number | null = null) { + if (finishCallback == -1000) { //no callback, used in twr_audio_play_range_full + finishCallback = null; + } + let [promise, id] = this.internalAudioPlayRange(mod, nodeID, startSample, endSample, loop, sampleRate, volume, pan); + if (finishCallback != null) { + promise.then((playback_id) => { + mod.postEvent(finishCallback, playback_id); + }); + } + + return id; + } + + async twrAudioPlaySync_async(mod: IWasmModuleAsync, nodeID: number, volume: number = 1, pan: number = 0) { + return this.twrAudioPlayRangeSync_async(mod, nodeID, 0, null, false, null, volume, pan,); + } + async twrAudioPlayRangeSync_async(mod: IWasmModuleAsync, nodeID: number, startSample: number, endSample: number | null = null, loop: boolean = false, sampleRate: number | null = null, volume: number = 1, pan: number = 0) { + let [promise, id] = this.internalAudioPlayRange(mod, nodeID, startSample, endSample, loop, sampleRate, volume, pan); + + await promise; + + return id; + } + + //queries current playback positions + //if the given ID doesn't exist, assume it was removed because it ended and return -1 + twrAudioQueryPlaybackPosition(mod: IWasmModuleAsync|IWasmModule, playbackID: number) { + if (!(playbackID in this.playbacks)) return -1; + + const playback = this.playbacks[playbackID]; + + switch (playback[0]) { + case NodeType.AudioBuffer: + { + return ((new Date()).getTime() - playback[2]); + } + break; + + case NodeType.HTMLAudioElement: + { + return playback[1].currentTime; + } + break; + + default: + throw new Error(`twrAudioQueryPlaybackPosition unknown type! ${playback[0]}`); + } + } + + async internalLoadAudio(mod: IWasmModuleAsync|IWasmModule, urlPtr: number, id: number) { + const url = mod.wasmMem.getString(urlPtr); + const res = await fetch(url); + + const buffer = await this.context.decodeAudioData(await res.arrayBuffer()); + this.nodes[id] = [NodeType.AudioBuffer, buffer]; + + } + async twrAudioLoadSync_async(mod: IWasmModuleAsync, urlPtr: number) { + const id = this.nextID++; + await this.internalLoadAudio(mod, urlPtr, id); + return id; + } + + twrAudioLoad(mod: IWasmModuleAsync|IWasmModule, eventID: number, urlPtr: number) { + const id = this.nextID++; + + this.internalLoadAudio(mod, urlPtr, id).then(() => { + mod.postEvent(eventID, id); + }); + + + return id; + } + + twrAudioFreeID(mod: IWasmModule|IWasmModuleAsync, nodeID: number) { + if (!(nodeID in this.nodes)) throw new Error(`twrAudioFreeID couldn't find node of ID ${nodeID}`); + + delete this.nodes[nodeID]; + } + + + // need to clarify some implementation details + twrAudioGetMetadata(mod: IWasmModuleAsync|IWasmModule, nodeID: number, metadataPtr: number) { + if (!(nodeID in this.nodes)) throw new Error(`twrAudioGetMetadata couldn't find node of ID ${nodeID}`); + + /* + struct AudioMetadata { + long length; + long sample_rate; + long channels; + };*/ + + const node = this.nodes[nodeID]; + + switch (node[0]) { + case NodeType.AudioBuffer: + { + mod.wasmMem.setLong(metadataPtr+0, node[1].length); + mod.wasmMem.setLong(metadataPtr+4, node[1].sampleRate); + mod.wasmMem.setLong(metadataPtr+8, node[1].numberOfChannels); + } + break; + + + default: + throw new Error(`twrAudioGetMetadata unknown type! ${node[0]}`); + } + + } + + twrAudioStopPlayback(mod: IWasmModule|IWasmModuleAsync, playbackID: number) { + if (!(playbackID in this.playbacks)) { + console.log(`Warning: twrAudioStopPlayback was given an ID that didn't exist (${playbackID})!`); + return; + } + + const node = this.playbacks[playbackID]; + + // console.log("hi!!"); + + switch (node[0]) { + case NodeType.AudioBuffer: + { + node[1].stop(); + } + break; + + case NodeType.HTMLAudioElement: + { + node[1].loop = false; + node[1].currentTime = Number.MAX_SAFE_INTEGER; + //delete index just in case audio hasn't loaded yet + delete this.playbacks[playbackID]; + } + break; + + + default: + throw new Error(`twrAudioStopPlayback unknown type! ${node[0]}`); + } + // delete this.playbacks[playbackID]; + } + + twrAudioModifyPlaybackVolume(mod: IWasmModule|IWasmModuleAsync, playbackID: number, volume: number) { + if (!(playbackID in this.playbacks)) { + console.log(`Warning: twrAudioModifyPlaybackVolume was given an ID that didn't exist (${playbackID})!`); + return; + } + + const node = this.playbacks[playbackID]; + if (volume > 1 || volume < 0) { + console.log(`Warning! twrAudioModifyPlaybackVolume was given a volume (${volume}) that wasn't between 0 and 1!`) + volume = Math.max(Math.min(volume, 1), 0); + } + + switch (node[0]) { + case NodeType.AudioBuffer: + { + const gainNode = node[4]; + gainNode.gain.value = volume; + } + break; + + case NodeType.HTMLAudioElement: + { + const audio = node[1]; + audio.volume = volume; + } + break; + } + } + + twrAudioModifyPlaybackPan(mod: IWasmModule|IWasmModuleAsync, playbackID: number, pan: number) { + if (!(playbackID in this.playbacks)) { + console.log(`Warning: twrAudioModifyPlaybackPan was given an ID that didn't exist (${playbackID})!`); + return; + } + + const node = this.playbacks[playbackID]; + + switch (node[0]) { + case NodeType.AudioBuffer: + { + const panNode = node[5]; + panNode.pan.value = pan; + } + break; + case NodeType.HTMLAudioElement: + { + throw new Error("Can't modify the pan of a playback started by twrAudioPlayFile!"); + } + break; + } + } + + twrAudioModifyPlaybackRate(mod: IWasmModule|IWasmModuleAsync, playbackID: number, sampleRate: number) { + if (!(playbackID in this.playbacks)) { + console.log(`Warning: twrAudioModifyPlaybackRate was given an ID that didn't exist (${playbackID})!`); + return; + } + + const node = this.playbacks[playbackID]; + + switch (node[0]) { + case NodeType.AudioBuffer: + { + const playback = node[1]; + const baseSampleRate = node[3]; + playback.playbackRate.value = sampleRate/baseSampleRate; + } + break; + case NodeType.HTMLAudioElement: + { + const audio = node[1]; + audio.playbackRate = sampleRate; + } + break; + } + } + + twrAudioPlayFile(mod: IWasmModule|IWasmModuleAsync, fileURLPtr: number, volume: number = 1.0, playbackRate: number = 1.0, loop: boolean = false) { + const playbackID = this.nextPlaybackID++; + + const fileURL = mod.wasmMem.getString(fileURLPtr); + const audio = new Audio(fileURL); + + audio.volume = volume; + audio.loop = loop; + audio.playbackRate = playbackRate; + + audio.onended = () => { + delete this.playbacks[playbackID]; + }; + + audio.play(); + + this.playbacks[playbackID] = [NodeType.HTMLAudioElement, audio]; + + return playbackID; + } +} \ No newline at end of file diff --git a/source/twr-ts/twrlibbuiltin.ts b/source/twr-ts/twrlibbuiltin.ts new file mode 100644 index 00000000..a9b1449f --- /dev/null +++ b/source/twr-ts/twrlibbuiltin.ts @@ -0,0 +1,29 @@ +// pre-installed libraries go in this file + +import {twrLibraryInstanceRegistry} from "./twrlibrary" + +import twrLibMathMod from "./twrlibmath.js"; +import twrLibLocaleMod from "./twrliblocale.js" +import twrLibTimerMod from "./twrlibtimer.js" + +import twrLibAudio from "./twrlibaudio.js"; +import twrLibDateMod from "./twrlibdate.js" +import twrConsoleDummy from "./twrcondummy.js" + +// currently, libraries can only have one instance +let defaultLibsAreRegistered=false; + +export async function twrLibBuiltIns() { + if (!defaultLibsAreRegistered) { + + // add builtin libraries here: + new twrLibMathMod; // will register self in twrLibraryInstanceRegistry + new twrLibLocaleMod; + new twrLibTimerMod; + new twrLibAudio; + new twrLibDateMod; + new twrConsoleDummy; + + defaultLibsAreRegistered=true; + } +} diff --git a/source/twr-ts/twrlibdate.ts b/source/twr-ts/twrlibdate.ts new file mode 100644 index 00000000..2f6833d9 --- /dev/null +++ b/source/twr-ts/twrlibdate.ts @@ -0,0 +1,31 @@ +import {IWasmModule} from "./twrmod.js" +import {twrWasmBase} from "./twrwasmbase.js" +import {twrLibrary, TLibImports, twrLibraryInstanceRegistry} from "./twrlibrary.js"; + +//TODO!! Add more Date functions (as alternatives to std c lib)? + +// add built-in Libraries (like this one) to twrLibBultins +// libraries use the default export +export default class twrLibDate extends twrLibrary { + id:number; + + imports:TLibImports = { + twrTimeEpoch:{isCommonCode: true} + } + + libSourcePath = new URL(import.meta.url).pathname; + + constructor() { + // all library constructors should start with these two lines + super(); + this.id=twrLibraryInstanceRegistry.register(this); + } + + // return ms since epoch as int64_t + twrTimeEpoch(callingMod:IWasmModule|twrWasmBase) { + + return BigInt(Date.now()); + + } +} + diff --git a/source/twr-ts/twrliblocale.ts b/source/twr-ts/twrliblocale.ts new file mode 100644 index 00000000..5c3320bb --- /dev/null +++ b/source/twr-ts/twrliblocale.ts @@ -0,0 +1,592 @@ +import {twrLibrary, TLibImports, twrLibraryInstanceRegistry} from "./twrlibrary.js"; +import {IWasmMemory} from "./twrwasmmem.js"; +import {IWasmModule} from "./twrmod.js" +import {twrWasmBase} from "./twrwasmbase.js" + +/////////////////////////////////////////////////////////////////////////////////////// + +// these match C #defines in locale.h +export const codePageASCII=0; +export const codePage1252=1252; +export const codePageUTF8=65001; +export const codePageUTF32=12000; + +/////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////// + +export default class twrLibLocale extends twrLibrary { + id:number; + imports:TLibImports = { + twrUnicodeCodePointToCodePage:{isCommonCode: true}, + twrCodePageToUnicodeCodePoint:{isCommonCode: true}, + twrUserLanguage:{isCommonCode: true}, + twrTimeTmLocal:{isCommonCode: true}, + twrUserLconv:{isCommonCode: true}, + twrRegExpTest1252:{isCommonCode: true}, + twrToUpper1252:{isCommonCode: true}, + twrToLower1252:{isCommonCode: true}, + twrStrcoll:{isCommonCode: true}, + twrGetDtnames:{isCommonCode: true}, + } + + libSourcePath = new URL(import.meta.url).pathname; + + cpTranslate = new twrCodePageToUnicodeCodePoint(); + cpTranslate2 = new twrCodePageToUnicodeCodePoint(); + + constructor() { + // all library constructors should start with these two lines + super(); + this.id=twrLibraryInstanceRegistry.register(this); + } + + /////////////////////////////////////////////////////////////////////////////////////// + + twrCodePageToUnicodeCodePoint(callingMod:IWasmModule|twrWasmBase, c:number, codePage:number) { + return this.cpTranslate2.convert(c, codePage); + } + + twrUnicodeCodePointToCodePage(callingMod:IWasmModule|twrWasmBase, outstr:number, cp:number, codePage:number) { + const ru8=callingMod.wasmMem.stringToU8(String.fromCodePoint(cp), codePage); + callingMod.wasmMem.mem8.set(ru8, outstr); + return ru8.length; + } + + twrUserLanguage(callingMod:IWasmModule|twrWasmBase,) { + // navigator.language works in JS main thread and Worker thread + return callingMod.wasmMem.putString(navigator.language, codePageASCII); + + } + + // checks if the character c, when converted to a string, is matched by the passed in regexp string + // utf-8 version not needed since this function is used for a single byte ('char'), + // and non-ascii range utf-8 single byte are not valid + twrRegExpTest1252(callingMod:IWasmModule|twrWasmBase, regexpStrIdx:number, c:number) { + + const regexpStr=callingMod.wasmMem.getString(regexpStrIdx); + const regexp=new RegExp(regexpStr, 'u'); + const cstr:string = this.cpTranslate.decoder1252.decode(new Uint8Array([c])); + const r=regexp.test(cstr); + if (r) return 1; else return 0; + + } + + // utf-8 version not needed since this function is used for a single byte ('char'), + // and non-ascii range utf-8 single byte are not valid + twrToLower1252(callingMod:IWasmModule|twrWasmBase, c:number) { + + const cstr:string = this.cpTranslate.decoder1252.decode(new Uint8Array([c])); + const regexp=new RegExp("^\\p{Letter}$", 'u'); + if (regexp.test(cstr)) { + const r = to1252(cstr.toLocaleLowerCase()); + //console.log("twrToLower1252Impl: isLetter", c, cstr, cstr.codePointAt(0), cstr.toLocaleLowerCase(), cstr.toLocaleLowerCase().codePointAt(0), r); + return r; + } + else { + //console.log("twrToLower1252Impl: isNOTLetter", c, cstr, cstr.codePointAt(0)); + return c; + } + + } + + //utf-8 version not needed since this function is used for a single byte ('char'), + // and non-ascii range utf-8 single byte are not valid + twrToUpper1252(callingMod:IWasmModule|twrWasmBase, c:number) { + + const cstr:string = this.cpTranslate.decoder1252.decode(new Uint8Array([c])); + + if (cstr.codePointAt(0)==402) return c; // appears to be safari Version 15.6.1 (17613.3.9.1.16) bug -- this is ƒ + if (cstr.codePointAt(0)==181) return c; // appears to be safari Version 15.6.1 (17613.3.9.1.16) bug -- this is µ + if (cstr.codePointAt(0)==223) return c; // appears to be safari Version 15.6.1 (17613.3.9.1.16) bug -- this is ß' + + if (cstr=="µ") return c; // upper case version doesn't fit in 1252 + if (cstr=='ƒ') return c; // upper case version doesn't fit in 1252 + if (cstr=='ß') return c; // toLocaleUpperCase() will convert beta to SS + + const regexp=new RegExp("^\\p{Letter}$", 'u'); + if (regexp.test(cstr)) { + return to1252(cstr.toLocaleUpperCase()); + } + else { + return c; + } + + } + + twrStrcoll(callingMod:IWasmModule|twrWasmBase, lhs:number, rhs:number, codePage:number) { + const lhStr=callingMod.wasmMem.getString(lhs, undefined, codePage); + const rhStr=callingMod.wasmMem.getString(rhs, undefined, codePage); + + // c strcmp(): A positive integer if str1 is greater than str2. + // 1 if string 1 (lh) comes after string 2 (rh) + const collator = new Intl.Collator(); + const r = collator.compare(lhStr, rhStr); + + return r; + } + + ///////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////// + + //struct tm { + // int tm_sec; /* seconds after the minute [0-60] */ + // int tm_min; /* minutes after the hour [0-59] */ + // int tm_hour; /* hours since midnight [0-23] */ + // int tm_mday; /* day of the month [1-31] */ + // int tm_mon; /* months since January [0-11] */ + // int tm_year; /* years since 1900 */ + // int tm_wday; /* days since Sunday [0-6] */ + // int tm_yday; /* days since January 1 [0-365] */ + // int tm_isdst; /* Daylight Saving Time flag */ + // long tm_gmtoff; /* offset from UTC in seconds */ + // char *tm_zone; /* timezone abbreviation */ + //}; + + // fill in struct tm + // epcohSecs as 32bit int will overflow January 19, 2038. + twrTimeTmLocal(callingMod:IWasmModule|twrWasmBase, tmIdx:number, epochSecs:number) { + + const d=new Date(epochSecs*1000); + callingMod.wasmMem.setLong(tmIdx, d.getSeconds()); + callingMod.wasmMem.setLong(tmIdx+4, d.getMinutes()); + callingMod.wasmMem.setLong(tmIdx+8, d.getHours()); + callingMod.wasmMem.setLong(tmIdx+12, d.getDate()); + callingMod.wasmMem.setLong(tmIdx+16, d.getMonth()); + callingMod.wasmMem.setLong(tmIdx+20, d.getFullYear()-1900); + callingMod.wasmMem.setLong(tmIdx+24, d.getDay()); + callingMod.wasmMem.setLong(tmIdx+28, this.getDayOfYear(d)); + callingMod.wasmMem.setLong(tmIdx+32, this.isDst()); + callingMod.wasmMem.setLong(tmIdx+36, -d.getTimezoneOffset()*60); + callingMod.wasmMem.setLong(tmIdx+40, callingMod.wasmMem.putString(this.getTZ(d), codePageASCII)); + + } + + private getDayOfYear(date:Date) { + const start = new Date(date.getFullYear(), 0, 1); + const diff = date.getTime() - start.getTime(); // Difference in milliseconds + const oneDay = 1000 * 60 * 60 * 24; // Number of milliseconds in one day + const day = Math.floor(diff / oneDay); + return day; + } + + private isDst() { + const timeString = new Date().toLocaleTimeString('en-US', { timeZoneName: 'long' }); + if (timeString.includes('Daylight')) { + return 1; + } else { + return 0; + } + } + + private getTZ(date:Date) { + const timeZone = date.toLocaleTimeString('en-US', {timeZoneName: 'short'}).split(' ').pop(); + return timeZone?timeZone:"UTC"; + } + + ///////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////// + + private setAndPutString(mem: IWasmMemory, idx:number, sin:string, codePage:number) { + const stridx=mem.putString(sin, codePage); + mem.setLong(idx, stridx); + } + + ///////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////// + + //struct lconv { + // char *decimal_point; 0 + // char *thousands_sep; 4 + // char *grouping; 8 + // char *int_curr_symbol; 12 + // char *currency_symbol; 16 + // char *mon_decimal_point; 20 + // char *mon_thousands_sep; 24 + // char *mon_grouping; 28 + // char *positive_sign; 32 + // char *negative_sign; 36 + // char int_frac_digits; 40 + // char frac_digits; 44 + // char p_cs_precedes; 48 + // char p_sep_by_space; 52 + // char n_cs_precedes; 56 + // char n_sep_by_space; 60 + // char p_sign_posn; 64 + // char n_sign_posn; 68 + //}; + + twrUserLconv(callingMod:IWasmModule|twrWasmBase, lconvIdx:number, codePage:number) { + const locDec=this.getLocaleDecimalPoint(); + const locSep=this.getLocaleThousandsSeparator(); + this.setAndPutString(callingMod.wasmMem, lconvIdx+0, locDec, codePage); + this.setAndPutString(callingMod.wasmMem, lconvIdx+4, locSep, codePage); + this.setAndPutString(callingMod.wasmMem, lconvIdx+20, locDec, codePage); + this.setAndPutString(callingMod.wasmMem, lconvIdx+24, locSep, codePage); + this.setAndPutString(callingMod.wasmMem, lconvIdx+24, locSep, codePage); + this.setAndPutString(callingMod.wasmMem, lconvIdx+24, locSep, codePage); + this.setAndPutString(callingMod.wasmMem, lconvIdx+32, "+", codePage); + this.setAndPutString(callingMod.wasmMem, lconvIdx+36, "-", codePage); + this.setAndPutString(callingMod.wasmMem, lconvIdx+12, this.getLocalCurrencySymbol(), codePage); + this.setAndPutString(callingMod.wasmMem, lconvIdx+16, this.getLocalCurrencySymbol(), codePage); + } + + private getLocaleDecimalPoint() { + const formatter = new Intl.NumberFormat(); + + //console.log("dec resolvedOptions", formatter.resolvedOptions()); + + // Format a test number to find out the decimal point. + const formattedNumber = formatter.format(1.1); + //console.log("dec formattedNumber", formattedNumber); + + // Find the character between the numeric parts. + const decimalPoint = formattedNumber.replace(/[0-9]/g, '').charAt(0); + + return decimalPoint; + } + + private getLocaleThousandsSeparator() { + const formatter = new Intl.NumberFormat(undefined, { + minimumFractionDigits: 0 // Ensure no decimal part interferes + }); + + // Format a test number to include a thousands separator. + const formattedNumber = formatter.format(1000); + //console.log("sep formattedNumber", formattedNumber); + + // Extract the thousands separator by removing numeric characters and possible decimal points. + // This may need adjustment depending on whether other characters are present. + let thousandsSeparator = formattedNumber.replace(/[0-9]/g, '').charAt(0); // Assumes separator is the first character. + //console.log("sep code", thousandsSeparator.codePointAt(0)); + return thousandsSeparator; + } + + // this doesn't work, localeCurrency is not correct + private getLocaleCurrencyDecimalPoint() { + // Create an initial NumberFormat object to detect the locale's currency + const tempFormatter = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD' }); + const localeCurrency = tempFormatter.resolvedOptions().currency; + const formatter = new Intl.NumberFormat(undefined, { + style: 'currency', + currency: localeCurrency + }); + // Format a test number to find out the decimal point. + const formattedNumber = formatter.format(1.1); + + // Find the character between the numeric parts. + // char(0) is the currency symbol + const decimalPoint = formattedNumber.replace(/[0-9]/g, '').charAt(1); + + return decimalPoint; + } + + private getLocalCurrencySymbol() { + switch (navigator.language) { + case "en-US": + case "en-CA": + case "fr-CA": + case "en-AU": + case "es-MX": + case "es-AR": + case "es-CL": + case "es-CO": + case "es-EC": + case "en-GY": + case "nl-SR": + case "es-UY": + case "en-BZ": + case "es-SV": + case "es-PA": + return "$"; + + case "es-BO": + case "es-VE": + return "Bs."; + + case "es-PY": + return "₲"; + + case "es-PE": + return "S/"; + + case "es-CR": + return "₡"; + + case "es-GT": + return "Q"; + + case "es-HN": + return "L"; + + case "es-NI": + return "C$"; + + case "en-GB": + return "£" + + case "en-IE": + case "de-DE": + case "fr-FR": + case "de-AT": + case "nl-BE": + case "fr-BE": + case "el-CY": + case "et-EE": + case "fi-FI": + case "sv-FI": + case "el-GR": + case "it-IT": + case "lv-LV": + case "lt-LT": + case "fr-LU": + case "de-LU": + case "lb-LU": + case "mt-MT": + case "nl-NL": + case "pt-PT": + case "sk-SK": + case "sl-SI": + case "es-ES": + return "€" + + case "ja-JP": + return "¥" + + case "zh-CN": + return "¥" + + case "de-CH": + case "fr-CH": + case "it-CH": + return "CHF" + + case "sv-SE": + case "da-DK": + case "nb-NO": + return "kr" + + case "ru-RU": + return "₽" + + case "ko-KR": + return "₩" + + case "en-IN": + return "₹" + + case "pt-BR": + return "R$" + + case "he-IL": + return "₪" + + case "tr-TR": + return "₺" + + default: + return ""; + } + } + + + ///////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////// + + /* + struct locale_dtnames { + const char* day[7]; + const char* abday[7]; + const char* month[12]; + const char* abmonth[12]; + const char* ampm[2]; + }; + */ + + twrGetDtnames(callingMod:IWasmModule|twrWasmBase, codePage:number) { + + const malloc=callingMod.wasmMem.malloc; + const dtnamesStructIdx:number=malloc(40*4); + for (let i=0; i<7; i++) + this.setAndPutString(callingMod.wasmMem, dtnamesStructIdx+i*4, this.getLocalizedDayName(i, 'long'), codePage); + + for (let i=0; i<7; i++) + this.setAndPutString(callingMod.wasmMem, dtnamesStructIdx+(i+7)*4, this.getLocalizedDayName(i, 'short'), codePage); + + for (let i=0; i<12; i++) + this.setAndPutString(callingMod.wasmMem, dtnamesStructIdx+(i+14)*4, this.getLocalizedMonthNames(i, 'long'), codePage); + + for (let i=0; i<12; i++) + this.setAndPutString(callingMod.wasmMem, dtnamesStructIdx+(i+14+12)*4, this.getLocalizedMonthNames(i, 'short'), codePage); + + this.setAndPutString(callingMod.wasmMem, dtnamesStructIdx+(0+14+24)*4, this.getLocalizedAM(), codePage); + this.setAndPutString(callingMod.wasmMem, dtnamesStructIdx+(1+14+24)*4, this.getLocalizedPM(), codePage); + + return dtnamesStructIdx; + } + + private getLocalizedDayName(n:number, weekdayType:'long'|'short') { + // Create a Date object for the desired day of the week + const date = new Date(); + date.setDate(date.getDate() - date.getDay() + n); + + // Create an Intl.DateTimeFormat object with the desired locale and options + const formatter = new Intl.DateTimeFormat(undefined, { weekday: weekdayType }); + + // Format the date to get the full day name + return formatter.format(date); + } + + private getLocalizedMonthNames(n:number, monthType:'long'|'short') { + const formatter = new Intl.DateTimeFormat(undefined, { month: monthType }); + const date = new Date(2000, n, 1); + return formatter.format(date); + } + + private getLocalizedAM() { + // Create a Date object for a time in the morning + const morningDate = new Date(2000, 0, 1, 9, 0, 0); + + // Create an Intl.DateTimeFormat object with the desired locale and options + const formatter = new Intl.DateTimeFormat(undefined, { + hour: 'numeric', + hour12: true + }); + + // Format the date and get the parts + const formattedParts = formatter.formatToParts(morningDate); + + // Find the part of the formatted string that corresponds to the day period (AM/PM) + const dayPeriodPart = formattedParts.find(part => part.type === 'dayPeriod'); + + return dayPeriodPart ? dayPeriodPart.value : ''; + } + + private getLocalizedPM() { + // Create a Date object for a time in the afternoon + const afternoonDate = new Date(2000, 0, 1, 15, 0, 0); + + // Create an Intl.DateTimeFormat object with the desired locale and options + const formatter = new Intl.DateTimeFormat(undefined, { + hour: 'numeric', + hour12: true + }); + + // Format the date and get the parts + const formattedParts = formatter.formatToParts(afternoonDate); + + // Find the part of the formatted string that corresponds to the day period (AM/PM) + const dayPeriodPart = formattedParts.find(part => part.type === 'dayPeriod'); + + return dayPeriodPart ? dayPeriodPart.value : ''; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////// + + +export function to1252(instr:string) { + + if (instr.codePointAt(0)==8239) return 32; // turn narrow-no-break-space into space + + + // this first switch statment fixes what appears to be a bug in safari 15.6.1 (17613.3.9.1.16) (comparisons to the character string fail) + let cp=instr.codePointAt(0) || 0; + + switch(cp) { + case 338: return 0x8C; + case 339: return 0x9C; + case 352: return 0x8A; + case 353: return 0x9A; + case 376: return 0x9F; + case 381: return 0x8E; + case 382: return 0x9E; + case 402: return 0x83; + case 710: return 0x88; + } + + switch (instr.normalize()) { + case '€': return 0x80; + case '‚': return 0x82; + case 'ƒ': return 0x83; + case '„': return 0x84; + case '…': return 0x85; + case '†': return 0x86; + case '‡': return 0x87; + case 'ˆ': return 0x88; + case '‰': return 0x89; + case 'Š': return 0x8A; + case '‹': return 0x8B; + case 'Œ': return 0x8C; + case 'Ž': return 0x8E; + case '‘': return 0x91; + case '’': return 0x92; + case '“': return 0x93; + case '”': return 0x94; + case '•': return 0x95; + case '–': return 0x96; + case '—': return 0x97; + case '˜': return 0x98; + case '™': return 0x99; + case 'š': return 0x9A; + case '›': return 0x9B; + case 'œ': return 0x9C; + case 'ž': return 0x9E; + case 'Ÿ': return 0x9F; + } + + if (cp>255) { + console.log("twr-wasm.to1252(): unable to convert: ", instr, cp); + cp=0; + } + + return cp; +} + +/////////////////////////////////////////////////////////////////////////////////////////////// + + +export function toASCII(instr:string) { + if (instr=='ƒ') return 102; // lowercase 'f' + if (instr.codePointAt(0)==8239) return 32; // turn narrow-no-break-space into space + + let cp=instr.codePointAt(0) || 0; + if (cp>127) return 63; // ASCII for "?" + return cp; +} + +/////////////////////////////////////////////////////////////////////////////////////////////// + +export class twrCodePageToUnicodeCodePoint { + decoderUTF8 = new TextDecoder('utf-8'); + decoder1252 = new TextDecoder('windows-1252'); + + convert(c:number, codePage:number) { + let outstr:string; + if (codePage==codePageUTF8) { + outstr=this.decoderUTF8.decode(new Uint8Array([c]), {stream: true}); + } + else if (codePage==codePage1252) { + outstr = this.decoder1252.decode(new Uint8Array([c])); + } + else if (codePage==codePageASCII) { + if (c>127) outstr=""; + else outstr=String.fromCharCode(c); + } + else if (codePage==codePageUTF32) { + outstr=String.fromCodePoint(c); + } + else { + throw new Error("unsupported CodePage: "+codePage) + } + + return outstr.codePointAt(0) || 0; + } +} diff --git a/source/twr-ts/twrlibmath.ts b/source/twr-ts/twrlibmath.ts new file mode 100644 index 00000000..40d06024 --- /dev/null +++ b/source/twr-ts/twrlibmath.ts @@ -0,0 +1,218 @@ +import {IWasmModule} from "./twrmod.js" +import {twrWasmBase} from "./twrwasmbase.js" +import {IWasmMemoryBase} from "./twrwasmmem"; +import {twrLibrary, TLibImports, twrLibraryInstanceRegistry} from "./twrlibrary.js"; + +// add built-in Libraries (like this one) to twrLibBultins +// libraries use the default export +export default class twrLibMath extends twrLibrary { + id:number; + imports:TLibImports = { + twrSin:{isCommonCode: true}, + twrCos:{isCommonCode: true}, + twrTan:{isCommonCode: true}, + twrACos:{isCommonCode: true}, + twrASin:{isCommonCode: true}, + twrATan:{isCommonCode: true}, + twrATan2: {isCommonCode: true}, + twrFAbs:{isCommonCode: true}, + twrExp:{isCommonCode: true}, + twrFloor:{isCommonCode: true}, + twrCeil:{isCommonCode: true}, + twrLog:{isCommonCode: true}, + twrSqrt:{isCommonCode: true}, + twrTrunc:{isCommonCode: true}, + twrFMod:{isCommonCode: true}, + twrPow:{isCommonCode: true}, + twrAtod:{isCommonCode: true}, + twrDtoa:{isCommonCode: true}, + twrToFixed:{isCommonCode: true}, + twrToExponential:{isCommonCode: true}, + twrFcvtS:{isCommonCode: true}, + }; + + libSourcePath = new URL(import.meta.url).pathname; + + constructor() { + // all library constructors should start with these two lines + super(); + this.id=twrLibraryInstanceRegistry.register(this); + } + + ////////////////////////////////////////////////////////////////////////////////////// + + // isCommonCode is set -- this means these functions will be used by twrWasmModuleAsync as well as twrWasmModule + // This means the functions in twrWasmModuleAsync will not RPC into the JS main thread. + // isCommonCode can only be used for functions that runs in a Worker thread, and that don't require the async keyword. + twrSin(callingMod:IWasmModule|twrWasmBase, angle:number ) {return Math.sin(angle);} + twrCos(callingMod:IWasmModule|twrWasmBase, angle:number ) {return Math.cos(angle);} + twrTan(callingMod:IWasmModule|twrWasmBase, angle:number ) {return Math.tan(angle);} + twrACos(callingMod:IWasmModule|twrWasmBase, p:number ) {return Math.acos(p);} + twrASin(callingMod:IWasmModule|twrWasmBase, p:number ) {return Math.asin(p);} + twrATan(callingMod:IWasmModule|twrWasmBase, p:number ) {return Math.atan(p);} + twrATan2(callingMod:IWasmModule|twrWasmBase, y:number, x:number ) {return Math.atan2(y, x)} + twrFAbs(callingMod:IWasmModule|twrWasmBase, p:number ) {return Math.abs(p);} + twrExp(callingMod:IWasmModule|twrWasmBase, p:number ) {return Math.exp(p);} + twrFloor(callingMod:IWasmModule|twrWasmBase, p:number ) {return Math.floor(p);} + twrCeil(callingMod:IWasmModule|twrWasmBase, p:number ) {return Math.ceil(p);} + twrLog(callingMod:IWasmModule|twrWasmBase, p:number ) {return Math.log(p);} + twrSqrt(callingMod:IWasmModule|twrWasmBase, p:number ) {return Math.sqrt(p);} + twrTrunc(callingMod:IWasmModule|twrWasmBase, p:number ) {return Math.trunc(p);} + twrFMod(callingMod:IWasmModule|twrWasmBase, x:number, y:number) {return x%y} + twrPow(callingMod:IWasmModule|twrWasmBase, x:number, y:number ) {return Math.pow(x,y);} + + twrAtod(callingMod:IWasmModule|twrWasmBase, ...p:[number, number]) {return this.atod(callingMod.wasmMem, ...p)} + twrDtoa(callingMod:IWasmModule|twrWasmBase, ...p:[number, number, number, number]) {this.dtoa(callingMod.wasmMem, ...p)} + twrToFixed(callingMod:IWasmModule|twrWasmBase, ...p:[number, number, number, number]) {this.toFixed(callingMod.wasmMem, ...p)} + twrToExponential(callingMod:IWasmModule|twrWasmBase, ...p:[number, number, number, number]) {this.toExponential(callingMod.wasmMem, ...p)} + twrFcvtS(callingMod:IWasmModule|twrWasmBase, ...p:[number, number, number, number, number, number]) {return this.fcvtS(callingMod.wasmMem, ...p)} + + ////////////////////////////////////////////////////////////////////////////////////// + + private atod(mem:IWasmMemoryBase, strptr:number, len:number):number { + const str=mem.getString(strptr, len); + + const upper=str.trimStart().toUpperCase(); + if (upper=="INF" || upper=="+INF" || upper=="INFINITY" || upper=="+INFINITY") + return Number.POSITIVE_INFINITY; + else if (upper=="-INF" || upper=="-INFINITY") + return Number.NEGATIVE_INFINITY + else { + // allow D for exponent -- old microsoft format they still support in _fctv and I support in my awbasic + // parseFloat will handle 'Infinity' and'-Infinity' literal + // parseFloat appears to be case sensitive when parsing 'Infinity' + // parseFloat ignores leading whitespace + // parseFloat() is more lenient than Number() because it ignores trailing invalid characters + const r=Number.parseFloat(str.replaceAll('D','e').replaceAll('d','e')); + return r; + } + } + + private dtoa(mem:IWasmMemoryBase, buffer:number, buffer_size:number, value:number, max_precision:number):void { + if (max_precision==-1) { + const r=value.toString(); + mem.copyString(buffer, buffer_size, r); + } + else { + let r=value.toString(); + if (r.length>max_precision) + r=value.toPrecision(max_precision); + mem.copyString(buffer, buffer_size, r); + } + } + + private toFixed(mem:IWasmMemoryBase, buffer:number, buffer_size:number, value:number, decdigits:number):void { + const r=value.toFixed(decdigits); + mem.copyString(buffer, buffer_size, r); + } + + private toExponential(mem:IWasmMemoryBase, buffer:number, buffer_size:number, value:number, decdigits:number):void { + const r=value.toExponential(decdigits); + mem.copyString(buffer, buffer_size, r); + } + + // emulates the MS C lib function _fcvt_s, but doesn't support all ranges of number. + // Number.toFixed() has a max size of 100 fractional digits, and values must be less than 1e+21 + // Negative exponents must be now smaller than 1e-99 + // fully-function C version also int he source, but this is the version enabled by default + private fcvtS(mem:IWasmMemoryBase, + buffer:number, // char * + sizeInBytes:number, //size_t + value:number, // double + fracpart_numdigits:number, //int + dec:number, // int * + sign:number // int * + ):number { + + if (buffer==0 ||sign==0 || dec==0 || sizeInBytes<1) return 1; + + let digits:string; + let decpos:number; + let s=0; // default to positive + + + if (Number.isNaN(value)) { /* nan */ + digits="1#QNAN00000000000000000000000000000".slice(0, fracpart_numdigits+1); + decpos=1; + } + else if (!Number.isFinite(value)) { /* infinity */ + digits="1#INF00000000000000000000000000000".slice(0, fracpart_numdigits+1); + decpos=1; + } + else if (value==0) { + digits="000000000000000000000000000000000000".slice(0,fracpart_numdigits); + decpos=0; + } + + else { + + if (value<0) { + s=1; // negative + value=Math.abs(value); + } + + if (fracpart_numdigits>100 || value > 1e+21 || value < 1e-99) { + mem!.copyString(buffer, sizeInBytes, ""); + mem!.mem32[dec]=0; + return 1; + } + + const roundValStr=value.toFixed(fracpart_numdigits); + let [intPart="", fracPart=""] = roundValStr.split('.'); + if (intPart=="0") intPart=""; + + if (intPart.length>0) { // has integer part + decpos=intPart.length; + digits=intPart+fracPart; + } + else { // only a fraction + digits=fracPart.replace(/^0+/,""); // remove leading zeros + decpos=digits.length-fracPart.length; + } + } + + if (sizeInBytes-1 < digits.length) return 1; + mem.copyString(buffer, sizeInBytes, digits); + mem.setLong(dec, decpos); + mem.setLong(sign, s); + + return 0; + + /* + this version 'works' with larger numbers than using toFixed, but doesn't round correctly + + let decpos=0; + let digits:string; + if (value!=0) decpos=Math.floor(Math.log10(value))+1; + + if (decpos>0) { // has integer part + const intlen=Math.max(decpos, 0); + console.log("intlen=",intlen, "decpos=",decpos); + const [nonExponent, exponent=0] = value.toPrecision(intlen+fracpart_numdigits).split('e'); + digits=nonExponent.replace('.', ''); + digits=digits.replace(/^0+/,""); // remove leading zeros + } + else { // only a fraction + const intpart=Math.trunc(value); + const fracpart=value-intpart; + const prec=fracpart_numdigits- (-decpos); + console.log("prec=",prec); + if (prec<1) { + digits=""; + } + else { + const [nonExponent, exponent=0] = fracpart.toPrecision(prec).split('e'); + digits=nonExponent.replace('.', ''); + digits=digits.replace(/^0+/,""); + } + } + + console.log("fcvtS value",value,"fracpart_numdigits",fracpart_numdigits); + console.log('digits=',digits); + console.log('dec=',decpos); + console.log("sign=",s); + */ + + } + +} \ No newline at end of file diff --git a/source/twr-ts/twrlibrary.ts b/source/twr-ts/twrlibrary.ts new file mode 100644 index 00000000..f5d5ea31 --- /dev/null +++ b/source/twr-ts/twrlibrary.ts @@ -0,0 +1,380 @@ +import {IWasmModule} from "./twrmod.js" +import {IWasmModuleAsync} from "./twrmodasync.js" +import {twrWasmModuleAsyncProxy} from "./twrmodasyncproxy.js" +import {twrEventQueueReceive} from "./twreventqueue.js" + +///////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////// + +// TODO List +// Should we: Allow callbacks while blocking call is waiting? Not on "return value" waits, but on functions like sleep. +// add a function to allow twrWasmModuleAsync c code (like main) to process events without returning to JavaScript +// fix example/lib/out and .gitignore so correct build artifacts are checked in +// resolve fact that libraries with interfaces are passed in the "io" option. Eg Allow "libs" or other synonym. +// current implementation has no libs: (akin to io:). +// consider if twrcondummy should be removed. Search for TODO, there are multiple places needing fixing. Possible solutions: +// (a) have interfaceName also list every function in the interface. +// create placholder "internal error" functions when a console/library does not implement an interface function. +// (b) require each function in interface in list each import correctly (either add isUnused or add dummy functions with exception) +// (c) merge imports (this won't work because a complete set of functions might not be loaded by api user) +// also search for "TODO!! This is here to make twrcondummy.ts" +// changed conterm example to use debug -- either change back, or change index description +// deal with twrConGetIDFromNameImpl. Note that twr_register_callback and twrConGetIDFromNameImpl are added in two different places. Unify +// change callingMod:IWasmModule|IWasmModuleAsync to IWasmBase ? +// add IWasmBase instead of using twrWasmBase +// add IWasmModuleBase ? +// Consider and handle app exit (stop events from being posted post app exit) +// Add postEvent example that includes arguments +// Implement event loop processing (get_next_event, get_filter_event) +// Issue with above: how do I get the event parameters? +// implement event loop in twrWasmModule (currently only in twrWasmModuleAsync) ? +// Need better name collision prevention on imported functions +// Are too many inefficient tickleEventLoop being sent? +// add codepage arg to register callback? + +///////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////// + +export type TLibImports = { [key:string]: {isAsyncFunction?:boolean, isModuleAsyncOnly?:boolean, isCommonCode?:boolean, noBlock?:boolean}}; +export type TLibraryProxyParams = ["twrLibraryProxy", libID:number, imports:TLibImports, libSourcePath:string, interfaceName: string|undefined]; + +// TLibraryMessage is sent from twrWasmModuleAsyncProxy (worker thread) to twrWasmModuleAsync +export type TLibraryMessage = ["twrLibrary", libID:number, funcName:string, isAsyncOverride:boolean, returnValueEventID:number, ...args:any[]]; + +///////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////// + +export abstract class twrLibrary { + abstract id: number; + + // must be set by derived class to describe each library function. See docs. + abstract imports: TLibImports; + + // libSourcePath must be set like this: + // use "libSourcePath = new URL(import.meta.url).pathname" + // above works for both bundled and unbundled -- at least with parcel + // example: "/lib-js/twrlibmath.js" + abstract libSourcePath: string; + + // set to unique name if multiple instances allowed (must all expose the same interface) (e.g. consoles). + // When true, APIs will expect first arg to be library ID. + interfaceName?:string; + + constructor() { + } + + // the actual twrLibrary is created outside of a specific wasm module, so isn't paired to a specific module + // however, each call to getImports is paired to a specific wasm module + // getImports returns Wasm Module imports that will be added to this wasm module's WebAssembly.ModuleImports + // getImports expects that the derived class has created a "this.import" with a list of function names (as strings) + // getImports is called by twrWasmModule + getImports(callingMod:IWasmModule) { + if (callingMod.isTwrWasmModuleAsync) throw new Error("unsupported module type (expecting twrWasmModule"); + + let wasmImports:{[key:string]: Function}={}; + const derivedInstanceThis=(this as unknown) as {[key:string]:(mod:IWasmModule, ...params:any)=>void}; + + if (this.imports===undefined) throw new Error("twrLibrary derived class is missing imports."); + if (this.libSourcePath===undefined) throw new Error("twrLibrary derived class is missing libSourcePath."); + + for (let funcName in this.imports) { + if (this.imports[funcName].isModuleAsyncOnly) { + const nullFun=() => { + throw new Error("Invalid call to unimplemented twrLibrary 'import' function (isModuleAsyncOnly was used): "+funcName); + } + wasmImports[funcName]=nullFun; + } + else { + if (!derivedInstanceThis[funcName]) + throw new Error("twrLibrary 'import' function is missing: "+funcName); + + if (this.interfaceName) { + // in this case, this particular instance represents the class + // but the actual instance needs to be retrieved at runtime using the libID & registry + // since only once set of WasmImports is created for each class + + const libFunc = (funcName: string, mod:IWasmModule, libID:number, ...params: any[]):any => { + const lib=twrLibraryInstanceRegistry.getLibraryInstance(libID); + const derivedLib=(lib as unknown) as {[key:string]:(callingMod:IWasmModule, ...params:any)=>void}; + const f=derivedLib[funcName]; + if (!f) throw new Error(`Library function not found. id=${libID}, funcName=${funcName}`); + return f.call(derivedLib, mod, ...params); + } + + wasmImports[funcName]=libFunc.bind(null, funcName, callingMod); // rest of function args are also passed to libFunc when using bind + } + else { + wasmImports[funcName]=derivedInstanceThis[funcName].bind(this, callingMod); + } + } + } + + return wasmImports; + } + + // this function is called by twrWasmModuleAsync, and sent to the corresponding twrWasmModuleAsyncProxy + getProxyParams() : TLibraryProxyParams { + return ["twrLibraryProxy", this.id, this.imports, this.libSourcePath, this.interfaceName]; + } + + // called by twrWasmModuleAsync + async processMessageFromProxy(msg:TLibraryMessage, mod:IWasmModuleAsync) { + const [msgClass, libID, funcName, doAwait, returnValueEventID, ...params]=msg; + if (this.interfaceName && twrLibraryInstanceRegistry.getLibraryInstance(libID).libSourcePath!=this.libSourcePath) + throw new Error("internal error"); // should never happen + else if (libID!=this.id) throw new Error("internal error"); // should never happen + + if (!mod.isTwrWasmModuleAsync) throw new Error("internal error"); + + const libThis=twrLibraryInstanceRegistry.getLibraryInstance(libID); + const derivedInstance=(libThis as unknown) as {[key:string]: ( (mod:IWasmModuleAsync|IWasmModule, ...params:any[])=>any) }; + if (!derivedInstance[funcName]) throw new Error("twrLibrary derived class missing 'import' function: "+funcName); + + let retVal; + if (doAwait) + retVal=await derivedInstance[funcName](mod, ...params); + else + retVal=derivedInstance[funcName](mod, ...params); + + if (returnValueEventID>-1) // -1 means noBlock true + mod.eventQueueSend.postEvent(returnValueEventID, retVal); + } + +} + +///////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////// + +export class twrLibraryProxy { + id:number; + imports: TLibImports; + libSourcePath:string; + interfaceName?:string; + called=false; + + //every module instance has its own twrLibraryProxy + + constructor(params:TLibraryProxyParams) { + const [className, id, imports, libSourcePath, interfaceName] = params; + this.id=id; + this.imports=imports; + this.libSourcePath=libSourcePath; + this.interfaceName=interfaceName; + } + + private remoteProcedureCall(ownerMod:twrWasmModuleAsyncProxy, funcName:string, isAsyncFunction:boolean, returnValueEventID:number, interfaceName:string|undefined, ...args:any[]) { + let msg:TLibraryMessage; + + if (interfaceName) + msg=["twrLibrary", args[0], funcName, isAsyncFunction, returnValueEventID, ...args.slice(1)]; + else + msg=["twrLibrary", this.id, funcName, isAsyncFunction, returnValueEventID, ...args]; + + // postMessage sends message to the JS Main thread that created the twrModAsyncProxy thread + // the message processing code discriminates the destination instance by: "twrLibrary", this.id, + postMessage(msg); + //TODO!! a void return type isn't particularly supported -- it will presumably returned undefined from the JS function, + //which will put a zero into the Int32Array used for returnValue + + if (returnValueEventID==-1) { // -1 means noBlock true + return 0; + } + + const [id, retVals]=ownerMod.eventQueueReceive.waitEvent(returnValueEventID); + if (id!=returnValueEventID) throw new Error("internal error"); + if (retVals.length!=1) throw new Error("internal error"); + return retVals[0]; + } + + // getProxyImports is called by twrWasmModuleAsyncProxy + // it provides the functions that the twrWasmModuleAsync's C code will call + // these will RPC to the JS main thread (unless isCommonCode set) and then wait for a return value (unless noBlock) + async getProxyImports(ownerMod:twrWasmModuleAsyncProxy) { + if (this.called===true) throw new Error("getProxyImports should only be called once per twrLibraryProxy instance"); + this.called=true; + + let wasmImports:{[key:string]: Function}={}; + let libClass; + + // now for each twrLibrary import, create the functions that will be added to wasm module imports + for (let funcName in this.imports) { + + if (this.imports[funcName].isCommonCode) { + if (this.imports[funcName].isAsyncFunction) + throw new Error("isAsyncFunction can not be used with isCommonCode"); + if (libClass===undefined) { + if (this.libSourcePath===undefined) + throw new Error("undefined libSourcePath"); + const libMod=await import(this.libSourcePath); + libClass=new libMod.default; + } + wasmImports[funcName]=libClass[funcName].bind(libClass, ownerMod); + } + else { + if (this.imports[funcName].isAsyncFunction) { + wasmImports[funcName]=this.remoteProcedureCall.bind(this, ownerMod, funcName+"_async", this.imports[funcName].isAsyncFunction?true:false, this.imports[funcName].noBlock?-1:twrEventQueueReceive.registerEvent(), this.interfaceName); + } + else { + wasmImports[funcName]=this.remoteProcedureCall.bind(this, ownerMod, funcName, this.imports[funcName].isAsyncFunction?true:false, this.imports[funcName].noBlock?-1:twrEventQueueReceive.registerEvent(), this.interfaceName); + } + } + } + + return wasmImports; + } +} + +///////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////// + +// this is global in the JS main thread address space +// all libraries are registered here +export class twrLibraryInstanceRegistry { + + // every twrLibrary instance goes here + static libInstances: twrLibrary[]=[]; + + // Each unique interface has one representative and arbitrary instance in libInterfaceInstances. + // A unique interfaceName represents a unique interface. Multiple classes may have the same interfaceName. + // (A class is identified by libSourcePath) + // An undefined interfaceName (anonymous interface) means that only one instance of that class is allowed + // and also means that the class has a unique anonymous interface. + static libInterfaceInstances: twrLibrary[]=[]; + + // create a pairing between an instance of type ILibraryBase and an integer ID + static register(libInstance:twrLibrary) { + + if (libInstance.imports===undefined) throw new Error("twrLibrary derived class is missing imports."); + if (libInstance.libSourcePath===undefined) throw new Error("twrLibrary derived class is missing libSourcePath."); + + // register the new instance + twrLibraryInstanceRegistry.libInstances.push(libInstance); + const id=twrLibraryInstanceRegistry.libInstances.length-1; + + // if this has a named interface, add it to the interface list, but only add it once. + if (libInstance.interfaceName) { + const interfaceID=this.getLibraryInstanceByInterfaceName(libInstance.interfaceName); + if (interfaceID===undefined) + twrLibraryInstanceRegistry.libInterfaceInstances.push(libInstance); + else { + // verify the interface are compatible. If they don't its a coding error + const alreadyRegisteredLibInstance=twrLibraryInstanceRegistry.libInterfaceInstances[interfaceID]; + for (let i=0; i Object.keys(alreadyRegisteredLibInstance.imports).length) + twrLibraryInstanceRegistry.libInterfaceInstances[interfaceID]=libInstance; + } + } + + // else this the type of Class that should only have a single instance + else { + // then check for the error where a Class is registered more than once + if (this.getLibraryInstanceByClass(libInstance.libSourcePath)) + throw new Error("A second twrLibrary instance was registered but interfaceName===undefined") + + // if no error, than add anonymous interface to the list + twrLibraryInstanceRegistry.libInterfaceInstances.push(libInstance); + } + + return id; + } + + static getLibraryInstance(id:number) { + if (id<0 || id >= twrLibraryInstanceRegistry.libInstances.length) + throw new Error("Invalid console ID: "+id); + + return twrLibraryInstanceRegistry.libInstances[id]; + } + + static getLibraryInstanceByInterfaceName(name:string) { + for (let i=0; i= twrLibraryInstanceProxyRegistry.libProxyInstances.length) + throw new Error("Invalid console ID: "+id); + + return twrLibraryInstanceProxyRegistry.libProxyInstances[id]; + } + + static getLibraryInstanceID(libProxyInstance:twrLibraryProxy) { + for (let i=0; i obj1[key] === obj2[key]); + } + +function CompareImports(obj1:TLibImports, obj2:TLibImports) { + const keys1 = Object.keys(obj1); + const keys2 = Object.keys(obj2); + + // they don't have to have the same number of imports, but every import that exists in both needs to match + for (let i=0; i{ + callingMod.postEvent(eventID) + }, milliSeconds); + } + + twr_timer_repeat(callingMod:IWasmModule|IWasmModuleAsync, milliSeconds:number, eventID:number) { + return setInterval(()=>{ + callingMod.postEvent(eventID) + }, milliSeconds); + } + + twr_timer_cancel(callingMod:IWasmModule|IWasmModuleAsync, timerID:number) { + clearInterval(timerID); + } + + async twr_sleep_async(callingMod:IWasmModuleAsync, milliSeconds:number) { + const p = new Promise( (resolve)=>{ + setTimeout(()=>{ resolve() }, milliSeconds); + }); + + return p; + } + +} + + + diff --git a/source/twr-ts/twrlocale.ts b/source/twr-ts/twrlocale.ts deleted file mode 100644 index c89811fb..00000000 --- a/source/twr-ts/twrlocale.ts +++ /dev/null @@ -1,562 +0,0 @@ -import {twrWasmModuleBase} from "./twrmodbase.js" - - -// these match C #defines in locale.h -export const codePageASCII=0; -export const codePage1252=1252; -export const codePageUTF8=65001; -export const codePageUTF32=12000; - -export class twrCodePageToUnicodeCodePoint { - decoderUTF8 = new TextDecoder('utf-8'); - decoder1252 = new TextDecoder('windows-1252'); - - convert(c:number, codePage:number) { - let outstr:string; - if (codePage==codePageUTF8) { - outstr=this.decoderUTF8.decode(new Uint8Array([c]), {stream: true}); - } - else if (codePage==codePage1252) { - outstr = this.decoder1252.decode(new Uint8Array([c])); - } - else if (codePage==codePageASCII) { - if (c>127) outstr=""; - else outstr=String.fromCharCode(c); - } - else if (codePage==codePageUTF32) { - outstr=String.fromCodePoint(c); - } - else { - throw new Error("unsupported CodePage: "+codePage) - } - - return outstr.codePointAt(0) || 0; - } -} - -const cpTranslate = new twrCodePageToUnicodeCodePoint(); - -export function twrUnicodeCodePointToCodePageImpl(this: twrWasmModuleBase, outstr:number, cp:number, codePage:number) { - return noasyncCopyString(this, outstr, String.fromCodePoint(cp), codePage); -} - -export function twrUserLanguageImpl(this: twrWasmModuleBase) { - - return noasyncPutString(this, navigator.language, codePageASCII); - -} - -// checks if the character c, when converted to a string, is matched by the passed in regexp string -// utf-8 version not needed since this function is used for a single byte ('char'), -// and non-ascii range utf-8 single byte are not valid -export function twrRegExpTest1252Impl(this: twrWasmModuleBase, regexpStrIdx:number, c:number) { - - const regexpStr=this.getString(regexpStrIdx); - const regexp=new RegExp(regexpStr, 'u'); - const cstr:string = cpTranslate.decoder1252.decode(new Uint8Array([c])); - const r=regexp.test(cstr); - if (r) return 1; else return 0; - -} - -export function to1252(instr:string) { - - if (instr.codePointAt(0)==8239) return 32; // turn narrow-no-break-space into space - - - // this first switch statment fixes what appears to be a bug in safari 15.6.1 (17613.3.9.1.16) (comparisons to the character string fail) - let cp=instr.codePointAt(0) || 0; - - switch(cp) { - case 338: return 0x8C; - case 339: return 0x9C; - case 352: return 0x8A; - case 353: return 0x9A; - case 376: return 0x9F; - case 381: return 0x8E; - case 382: return 0x9E; - case 402: return 0x83; - case 710: return 0x88; - } - - switch (instr.normalize()) { - case '€': return 0x80; - case '‚': return 0x82; - case 'ƒ': return 0x83; - case '„': return 0x84; - case '…': return 0x85; - case '†': return 0x86; - case '‡': return 0x87; - case 'ˆ': return 0x88; - case '‰': return 0x89; - case 'Š': return 0x8A; - case '‹': return 0x8B; - case 'Œ': return 0x8C; - case 'Ž': return 0x8E; - case '‘': return 0x91; - case '’': return 0x92; - case '“': return 0x93; - case '”': return 0x94; - case '•': return 0x95; - case '–': return 0x96; - case '—': return 0x97; - case '˜': return 0x98; - case '™': return 0x99; - case 'š': return 0x9A; - case '›': return 0x9B; - case 'œ': return 0x9C; - case 'ž': return 0x9E; - case 'Ÿ': return 0x9F; - } - - if (cp>255) { - console.log("twr-wasm.to1252(): unable to convert: ", instr, cp); - cp=0; - } - - return cp; -} - -export function toASCII(instr:string) { - if (instr=='ƒ') return 102; // lowercase 'f' - if (instr.codePointAt(0)==8239) return 32; // turn narrow-no-break-space into space - - let cp=instr.codePointAt(0) || 0; - if (cp>127) return 63; // ASCII for "?" - return cp; -} - -// utf-8 version not needed since this function is used for a single byte ('char'), -// and non-ascii range utf-8 single byte are not valid -export function twrToLower1252Impl(this: twrWasmModuleBase, c:number) { - - const cstr:string = cpTranslate.decoder1252.decode(new Uint8Array([c])); - const regexp=new RegExp("^\\p{Letter}$", 'u'); - if (regexp.test(cstr)) { - const r = to1252(cstr.toLocaleLowerCase()); - //console.log("twrToLower1252Impl: isLetter", c, cstr, cstr.codePointAt(0), cstr.toLocaleLowerCase(), cstr.toLocaleLowerCase().codePointAt(0), r); - return r; - } - else { - //console.log("twrToLower1252Impl: isNOTLetter", c, cstr, cstr.codePointAt(0)); - return c; - } - -} - -//utf-8 version not needed since this function is used for a single byte ('char'), -// and non-ascii range utf-8 single byte are not valid -export function twrToUpper1252Impl(this: twrWasmModuleBase, c:number) { - - const cstr:string = cpTranslate.decoder1252.decode(new Uint8Array([c])); - - if (cstr.codePointAt(0)==402) return c; // appears to be safari Version 15.6.1 (17613.3.9.1.16) bug -- this is ƒ - if (cstr.codePointAt(0)==181) return c; // appears to be safari Version 15.6.1 (17613.3.9.1.16) bug -- this is µ - if (cstr.codePointAt(0)==223) return c; // appears to be safari Version 15.6.1 (17613.3.9.1.16) bug -- this is ß' - - if (cstr=="µ") return c; // upper case version doesn't fit in 1252 - if (cstr=='ƒ') return c; // upper case version doesn't fit in 1252 - if (cstr=='ß') return c; // toLocaleUpperCase() will convert beta to SS - - const regexp=new RegExp("^\\p{Letter}$", 'u'); - if (regexp.test(cstr)) { - return to1252(cstr.toLocaleUpperCase()); - } - else { - return c; - } - -} - -export function twrStrcollImpl(this: twrWasmModuleBase, lhs:number, rhs:number, codePage:number) { - const lhStr=this.getString(lhs, undefined, codePage); - const rhStr=this.getString(rhs, undefined, codePage); - - // c strcmp(): A positive integer if str1 is greater than str2. - // 1 if string 1 (lh) comes after string 2 (rh) - const collator = new Intl.Collator(); - const r = collator.compare(lhStr, rhStr); - - return r; -} - -///////////////////////////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////////////////////////// - -//struct tm { -// int tm_sec; /* seconds after the minute [0-60] */ -// int tm_min; /* minutes after the hour [0-59] */ -// int tm_hour; /* hours since midnight [0-23] */ -// int tm_mday; /* day of the month [1-31] */ -// int tm_mon; /* months since January [0-11] */ -// int tm_year; /* years since 1900 */ -// int tm_wday; /* days since Sunday [0-6] */ -// int tm_yday; /* days since January 1 [0-365] */ -// int tm_isdst; /* Daylight Saving Time flag */ -// long tm_gmtoff; /* offset from UTC in seconds */ -// char *tm_zone; /* timezone abbreviation */ -//}; - -// fill in struct tm -// epcohSecs as 32bit int will overflow January 19, 2038. -export function twrTimeTmLocalImpl(this: twrWasmModuleBase, tmIdx:number, epochSecs:number) { - - const d=new Date(epochSecs*1000); - this.setLong(tmIdx, d.getSeconds()); - this.setLong(tmIdx+4, d.getMinutes()); - this.setLong(tmIdx+8, d.getHours()); - this.setLong(tmIdx+12, d.getDate()); - this.setLong(tmIdx+16, d.getMonth()); - this.setLong(tmIdx+20, d.getFullYear()-1900); - this.setLong(tmIdx+24, d.getDay()); - this.setLong(tmIdx+28, getDayOfYear(d)); - this.setLong(tmIdx+32, isDst()); - this.setLong(tmIdx+36, -d.getTimezoneOffset()*60); - this.setLong(tmIdx+40, noasyncPutString(this, getTZ(d), codePageASCII)); - -} - -function getDayOfYear(date:Date) { - const start = new Date(date.getFullYear(), 0, 1); - const diff = date.getTime() - start.getTime(); // Difference in milliseconds - const oneDay = 1000 * 60 * 60 * 24; // Number of milliseconds in one day - const day = Math.floor(diff / oneDay); - return day; -} - -function isDst() { - const timeString = new Date().toLocaleTimeString('en-US', { timeZoneName: 'long' }); - if (timeString.includes('Daylight')) { - return 1; - } else { - return 0; - } -} - -function getTZ(date:Date) { - const timeZone = date.toLocaleTimeString('en-US', {timeZoneName: 'short'}).split(' ').pop(); - return timeZone?timeZone:"UTC"; -} - -///////////////////////////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////////////////////////// - -function setAndPutString(mod: twrWasmModuleBase, idx:number, sin:string, codePage:number) { - const stridx=noasyncPutString(mod, sin, codePage); - mod.setLong(idx, stridx); -} - -// JS string into the webassembly module memory. -// Does not verify outbuf length. -// Encode the Wasm string using codePage -// Does NOT zero terminate string -function noasyncCopyString(mod: twrWasmModuleBase, outbuf:number, sin:string, codePage:number) { - const ru8=mod.stringToU8(sin, codePage); - mod.mem8.set(ru8, outbuf); - return ru8.length; -} - -// allocate and copy a JS string into the webassembly module memory, encode the Wasm string using codePage -function noasyncPutString(mod: twrWasmModuleBase, sin:string, codePage:number) { - const ru8=mod.stringToU8(sin, codePage); - const malloc=mod.exports!.malloc as (size:number)=>number; - const strIndex:number=malloc(ru8.length+1); - mod.mem8.set(ru8, strIndex); - mod.mem8[strIndex+ru8.length]=0; - - return strIndex; -} - -///////////////////////////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////////////////////////// - -//struct lconv { -// char *decimal_point; 0 -// char *thousands_sep; 4 -// char *grouping; 8 -// char *int_curr_symbol; 12 -// char *currency_symbol; 16 -// char *mon_decimal_point; 20 -// char *mon_thousands_sep; 24 -// char *mon_grouping; 28 -// char *positive_sign; 32 -// char *negative_sign; 36 -// char int_frac_digits; 40 -// char frac_digits; 44 -// char p_cs_precedes; 48 -// char p_sep_by_space; 52 -// char n_cs_precedes; 56 -// char n_sep_by_space; 60 -// char p_sign_posn; 64 -// char n_sign_posn; 68 -//}; - -export function twrUserLconvImpl(this: twrWasmModuleBase, lconvIdx:number, codePage:number) { - const locDec=getLocaleDecimalPoint(); - const locSep=getLocaleThousandsSeparator(); - setAndPutString(this, lconvIdx+0, locDec, codePage); - setAndPutString(this, lconvIdx+4, locSep, codePage); - setAndPutString(this, lconvIdx+20, locDec, codePage); - setAndPutString(this, lconvIdx+24, locSep, codePage); - setAndPutString(this, lconvIdx+24, locSep, codePage); - setAndPutString(this, lconvIdx+24, locSep, codePage); - setAndPutString(this, lconvIdx+32, "+", codePage); - setAndPutString(this, lconvIdx+36, "-", codePage); - setAndPutString(this, lconvIdx+12, getLocalCurrencySymbol(), codePage); - setAndPutString(this, lconvIdx+16, getLocalCurrencySymbol(), codePage); -} - -function getLocaleDecimalPoint() { - const formatter = new Intl.NumberFormat(); - - //console.log("dec resolvedOptions", formatter.resolvedOptions()); - - // Format a test number to find out the decimal point. - const formattedNumber = formatter.format(1.1); - //console.log("dec formattedNumber", formattedNumber); - - // Find the character between the numeric parts. - const decimalPoint = formattedNumber.replace(/[0-9]/g, '').charAt(0); - - return decimalPoint; -} - -function getLocaleThousandsSeparator() { - const formatter = new Intl.NumberFormat(undefined, { - minimumFractionDigits: 0 // Ensure no decimal part interferes - }); - - // Format a test number to include a thousands separator. - const formattedNumber = formatter.format(1000); - //console.log("sep formattedNumber", formattedNumber); - - // Extract the thousands separator by removing numeric characters and possible decimal points. - // This may need adjustment depending on whether other characters are present. - let thousandsSeparator = formattedNumber.replace(/[0-9]/g, '').charAt(0); // Assumes separator is the first character. - //console.log("sep code", thousandsSeparator.codePointAt(0)); - return thousandsSeparator; -} - -// this doesn't work, localeCurrency is not correct -function getLocaleCurrencyDecimalPoint() { - // Create an initial NumberFormat object to detect the locale's currency - const tempFormatter = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD' }); - const localeCurrency = tempFormatter.resolvedOptions().currency; - const formatter = new Intl.NumberFormat(undefined, { - style: 'currency', - currency: localeCurrency - }); - // Format a test number to find out the decimal point. - const formattedNumber = formatter.format(1.1); - - // Find the character between the numeric parts. - // char(0) is the currency symbol - const decimalPoint = formattedNumber.replace(/[0-9]/g, '').charAt(1); - - return decimalPoint; -} - -function getLocalCurrencySymbol() { - switch (navigator.language) { - case "en-US": - case "en-CA": - case "fr-CA": - case "en-AU": - case "es-MX": - case "es-AR": - case "es-CL": - case "es-CO": - case "es-EC": - case "en-GY": - case "nl-SR": - case "es-UY": - case "en-BZ": - case "es-SV": - case "es-PA": - return "$"; - - case "es-BO": - case "es-VE": - return "Bs."; - - case "es-PY": - return "₲"; - - case "es-PE": - return "S/"; - - case "es-CR": - return "₡"; - - case "es-GT": - return "Q"; - - case "es-HN": - return "L"; - - case "es-NI": - return "C$"; - - case "en-GB": - return "£" - - case "en-IE": - case "de-DE": - case "fr-FR": - case "de-AT": - case "nl-BE": - case "fr-BE": - case "el-CY": - case "et-EE": - case "fi-FI": - case "sv-FI": - case "el-GR": - case "it-IT": - case "lv-LV": - case "lt-LT": - case "fr-LU": - case "de-LU": - case "lb-LU": - case "mt-MT": - case "nl-NL": - case "pt-PT": - case "sk-SK": - case "sl-SI": - case "es-ES": - return "€" - - case "ja-JP": - return "¥" - - case "zh-CN": - return "¥" - - case "de-CH": - case "fr-CH": - case "it-CH": - return "CHF" - - case "sv-SE": - case "da-DK": - case "nb-NO": - return "kr" - - case "ru-RU": - return "₽" - - case "ko-KR": - return "₩" - - case "en-IN": - return "₹" - - case "pt-BR": - return "R$" - - case "he-IL": - return "₪" - - case "tr-TR": - return "₺" - - default: - return ""; - } -} - - -///////////////////////////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////////////////////////// - -/* -struct locale_dtnames { - const char* day[7]; - const char* abday[7]; - const char* month[12]; - const char* abmonth[12]; - const char* ampm[2]; -}; -*/ - -export function twrGetDtnamesImpl(this: twrWasmModuleBase, codePage:number) { - - const malloc=this.exports!.malloc as (size:number)=>number; - const dtnamesStructIdx:number=malloc(40*4); - for (let i=0; i<7; i++) - setAndPutString(this, dtnamesStructIdx+i*4, getLocalizedDayName(i, 'long'), codePage); - - for (let i=0; i<7; i++) - setAndPutString(this, dtnamesStructIdx+(i+7)*4, getLocalizedDayName(i, 'short'), codePage); - - for (let i=0; i<12; i++) - setAndPutString(this, dtnamesStructIdx+(i+14)*4, getLocalizedMonthNames(i, 'long'), codePage); - - for (let i=0; i<12; i++) - setAndPutString(this, dtnamesStructIdx+(i+14+12)*4, getLocalizedMonthNames(i, 'short'), codePage); - - setAndPutString(this, dtnamesStructIdx+(0+14+24)*4, getLocalizedAM(), codePage); - setAndPutString(this, dtnamesStructIdx+(1+14+24)*4, getLocalizedPM(), codePage); - - return dtnamesStructIdx; -} - -function getLocalizedDayName(n:number, weekdayType:'long'|'short') { - // Create a Date object for the desired day of the week - const date = new Date(); - date.setDate(date.getDate() - date.getDay() + n); - - // Create an Intl.DateTimeFormat object with the desired locale and options - const formatter = new Intl.DateTimeFormat(undefined, { weekday: weekdayType }); - - // Format the date to get the full day name - return formatter.format(date); -} - -function getLocalizedMonthNames(n:number, monthType:'long'|'short') { - const formatter = new Intl.DateTimeFormat(undefined, { month: monthType }); - const date = new Date(2000, n, 1); - return formatter.format(date); -} - -function getLocalizedAM() { - // Create a Date object for a time in the morning - const morningDate = new Date(2000, 0, 1, 9, 0, 0); - - // Create an Intl.DateTimeFormat object with the desired locale and options - const formatter = new Intl.DateTimeFormat(undefined, { - hour: 'numeric', - hour12: true - }); - - // Format the date and get the parts - const formattedParts = formatter.formatToParts(morningDate); - - // Find the part of the formatted string that corresponds to the day period (AM/PM) - const dayPeriodPart = formattedParts.find(part => part.type === 'dayPeriod'); - - return dayPeriodPart ? dayPeriodPart.value : ''; -} - -function getLocalizedPM() { - // Create a Date object for a time in the afternoon - const afternoonDate = new Date(2000, 0, 1, 15, 0, 0); - - // Create an Intl.DateTimeFormat object with the desired locale and options - const formatter = new Intl.DateTimeFormat(undefined, { - hour: 'numeric', - hour12: true - }); - - // Format the date and get the parts - const formattedParts = formatter.formatToParts(afternoonDate); - - // Find the part of the formatted string that corresponds to the day period (AM/PM) - const dayPeriodPart = formattedParts.find(part => part.type === 'dayPeriod'); - - return dayPeriodPart ? dayPeriodPart.value : ''; -} - diff --git a/source/twr-ts/twrmod.ts b/source/twr-ts/twrmod.ts index 81f58484..9b2ac2e7 100644 --- a/source/twr-ts/twrmod.ts +++ b/source/twr-ts/twrmod.ts @@ -1,128 +1,182 @@ -import {IModOpts} from "./twrmodbase.js"; -import {twrWasmModuleInJSMain} from "./twrmodjsmain.js" -import {twrTimeEpochImpl} from "./twrdate.js" -import {twrTimeTmLocalImpl, twrUserLconvImpl, twrUserLanguageImpl, twrRegExpTest1252Impl,twrToLower1252Impl, twrToUpper1252Impl} from "./twrlocale.js" -import {twrStrcollImpl, twrUnicodeCodePointToCodePageImpl, twrCodePageToUnicodeCodePoint, twrGetDtnamesImpl} from "./twrlocale.js" -import {IConsole} from "./twrcon.js" -import {twrConsoleRegistry} from "./twrconreg.js" +import {parseModOptions, IModOpts} from "./twrmodutil.js" +import {IConsole, logToCon} from "./twrcon.js" +import {twrLibraryInstanceRegistry} from "./twrlibrary.js"; +import {IWasmMemory} from './twrwasmmem.js' +import {twrWasmCall} from "./twrwasmcall.js" +import {twrWasmBase, TOnEventCallback} from "./twrwasmbase.js" +import {twrEventQueueReceive} from "./twreventqueue.js" +import {twrLibBuiltIns} from "./twrlibbuiltin.js" + +/*********************************************************************/ + +// Partial defines the deprecated, backwards compatible +// memory access APIs that are at the module level. +// New code should use wasmMem. +export interface IWasmModule extends Partial { + wasmMem: IWasmMemory; + wasmCall: twrWasmCall; + callC:twrWasmCall["callC"]; + isTwrWasmModuleAsync:false; // to avoid circular references -- check if twrWasmModule without importing twrWasmModule + //TODO!! put these into IWasmModuleBase (some could be implemented in twrWasmModuleBase, but many have different implementations) + loadWasm: (pathToLoad:string)=>Promise; + postEvent: TOnEventCallback; + fetchAndPutURL: (fnin:URL)=>Promise<[number, number]>; + divLog:(...params: string[])=>void; + log:(...params: string[])=>void; +} -export class twrWasmModule extends twrWasmModuleInJSMain { - malloc:(size:number)=>Promise; - imports:WebAssembly.ModuleImports; - cpTranslate:twrCodePageToUnicodeCodePoint; +/*********************************************************************/ + +export class twrWasmModule extends twrWasmBase implements IWasmModule { + io:{[key:string]: IConsole}; + ioNamesToID: {[key: string]: number}; + isTwrWasmModuleAsync:false=false; + + // divLog is deprecated. Use IConsole.putStr or log + divLog:(...params: string[])=>void; + log:(...params: string[])=>void; + + // IWasmMemory + // These are deprecated, use wasmMem instead. + memory!:WebAssembly.Memory; + mem8!:Uint8Array; + mem32!:Uint32Array; + memD!:Float64Array; + stringToU8!:(sin:string, codePage?:number)=>Uint8Array; + copyString!:(buffer:number, buffer_size:number, sin:string, codePage?:number)=>void; + getLong!:(idx:number)=>number; + setLong!:(idx:number, value:number)=>void; + getDouble!:(idx:number)=>number; + setDouble!:(idx:number, value:number)=>void; + getShort!:(idx:number)=>number; + getString!:(strIndex:number, len?:number, codePage?:number)=>string; + getU8Arr!:(idx:number)=>Uint8Array; + getU32Arr!:(idx:number)=>Uint32Array; + + malloc!:(size:number)=>number; + free!:(size:number)=>void; + putString!:(sin:string, codePage?:number)=>number; + putU8!:(u8a:Uint8Array)=>number; + putArrayBuffer!:(ab:ArrayBuffer)=>number; + + /*********************************************************************/ constructor(opts:IModOpts={}) { - super(opts); - this.malloc=(size:number)=>{throw new Error("error - un-init malloc called")}; - this.cpTranslate=new twrCodePageToUnicodeCodePoint(); - - const canvasErrorFunc = (...args: any[]):any => { - throw new Error("A 2D draw function was called, but a valid twrCanvas is not defined."); - } - - const conCall = (funcName: keyof IConsole, jsid:number, ...args: any[]):any => { - const con=twrConsoleRegistry.getConsole(jsid); - const f=con[funcName] as (...args: any[]) => any; - if (!f) throw new Error(`Likely using an incorrect console type. jsid=${jsid}, funcName=${funcName}`); - return f.call(con, ...args); - } + super(); + [this.io, this.ioNamesToID] = parseModOptions(opts); + this.log=logToCon.bind(undefined, this.io.stdio); + this.divLog=this.log; + } - const conSetRange = (jsid:number, chars:number, start:number, len:number) => { - let values=[]; - for (let i=start; i { - conCall("putStr", jsid, this.getString(chars), codePage); - } - - const conGetProp = (jsid:number, pn:number):number => { - const propName=this.getString(pn); - return conCall("getProp", jsid, propName); - } + async loadWasm(pathToLoad:string) { - const conDrawSeq = (jsid:number, ds:number) => { - conCall("drawSeq", jsid, ds, this); - } + // load builtin libraries + await twrLibBuiltIns(); - const twrGetConIDFromNameImpl = (nameIdx:number):number => { - const name=this.getString(nameIdx); + const twrConGetIDFromNameImpl = (nameIdx:number):number => { + const name=this.wasmMem!.getString(nameIdx); const id=this.ioNamesToID[name]; if (id) return id; else return -1; } - - this.imports={ - twrTimeEpoch:twrTimeEpochImpl, - twrTimeTmLocal:twrTimeTmLocalImpl.bind(this), - twrUserLconv:twrUserLconvImpl.bind(this), - twrUserLanguage:twrUserLanguageImpl.bind(this), - twrRegExpTest1252:twrRegExpTest1252Impl.bind(this), - twrToLower1252:twrToLower1252Impl.bind(this), - twrToUpper1252:twrToUpper1252Impl.bind(this), - twrStrcoll:twrStrcollImpl.bind(this), - twrUnicodeCodePointToCodePage:twrUnicodeCodePointToCodePageImpl.bind(this), - twrCodePageToUnicodeCodePoint:this.cpTranslate.convert.bind(this.cpTranslate), - twrGetDtnames:twrGetDtnamesImpl.bind(this), - twrGetConIDFromName: twrGetConIDFromNameImpl, - - twrConCharOut:conCall.bind(null, "charOut"), - twrConCharIn:this.null, - twrSetFocus:this.null, - - twrConGetProp:conGetProp, - twrConCls:conCall.bind(null, "cls"), - twrConSetC32:conCall.bind(null, "setC32"), - twrConSetReset:conCall.bind(null, "setReset"), - twrConPoint:conCall.bind(null, "point"), - twrConSetCursor:conCall.bind(null, "setCursor"), - twrConSetColors:conCall.bind(null, "setColors"), - twrConSetRange:conSetRange, - twrConPutStr:conPutStr, - - twrConDrawSeq:conDrawSeq, + + let imports:WebAssembly.ModuleImports={}; + for (let i=0; i { + + if (!(typeof fnin === 'object' && fnin instanceof URL)) + throw new Error("fetchAndPutURL param must be URL"); + + try { + let response=await fetch(fnin); + let buffer = await response.arrayBuffer(); + let src = new Uint8Array(buffer); + let dest=this.wasmMem.putU8(src); + return [dest, src.length]; - twrCanvasCharIn:this.null, - twrCanvasInkey:this.null, - twrSleep:this.null, - - twrSin:Math.sin, - twrCos:Math.cos, - twrTan: Math.tan, - twrFAbs: Math.abs, - twrACos: Math.acos, - twrASin: Math.asin, - twrATan: Math.atan, - twrExp: Math.exp, - twrFloor: Math.floor, - twrCeil: Math.ceil, - twrFMod: function(x:number, y:number) {return x%y}, - twrLog: Math.log, - twrPow: Math.pow, - twrSqrt: Math.sqrt, - twrTrunc: Math.trunc, - - twrDtoa: this.floatUtil.dtoa.bind(this.floatUtil), - twrToFixed: this.floatUtil.toFixed.bind(this.floatUtil), - twrToExponential: this.floatUtil.toExponential.bind(this.floatUtil), - twrAtod: this.floatUtil.atod.bind(this.floatUtil), - twrFcvtS: this.floatUtil.fcvtS.bind(this.floatUtil), + } catch(err:any) { + console.log('fetchAndPutURL Error. URL: '+fnin+'\n' + err + (err.stack ? "\n" + err.stack : '')); + throw err; } } - async loadWasm(pathToLoad:string) { - return super.loadWasm(pathToLoad, this.imports, this.ioNamesToID); + postEvent(eventID:number, ...params:number[]) { + //TODO!! PostEvent into eventQueueSend, then processEvents -- to enable non callback events when i add them + if (!(eventID in twrEventQueueReceive.onEventCallbacks)) + throw new Error("twrWasmModule.postEvent called with invalid eventID: "+eventID+", params: "+params); + + const onEventCallback=twrEventQueueReceive.onEventCallbacks[eventID]; + if (onEventCallback) + onEventCallback(eventID, ...params); + else + throw new Error("twrWasmModule.postEvent called with undefined callback. eventID: "+eventID+", params: "+params); } - null(inval?:any) { - throw new Error("call to unimplemented twrXXX import in twrWasmModule. Use twrWasmModuleAsync ?"); + peekEvent(eventName:string) { + // get earliest inserted entry in event Map + //const ev=this.events.get(eventName) } + // called (RPC) by twrLibraryProxy + // waitEvent(eventName:string) { + // const evIdx=peekTwrEvent(eventName); + // if (evIdx) { + // this.returnValue!.write(evIdx); + // } + // else { + // this.addEventListner(eventName, (evIdx:number)=> { + // this.returnValue!.write(evIdx); + // }); + // } } diff --git a/source/twr-ts/twrmodasync.ts b/source/twr-ts/twrmodasync.ts index 5fb7d0a9..4d334729 100644 --- a/source/twr-ts/twrmodasync.ts +++ b/source/twr-ts/twrmodasync.ts @@ -1,97 +1,136 @@ -import {IModOpts} from "./twrmodbase.js"; import {IAllProxyParams} from "./twrmodasyncproxy.js" -import {twrWasmModuleInJSMain} from "./twrmodjsmain.js" -import {twrWaitingCalls} from "./twrwaitingcalls.js" -import {IConsole, keyDownUtil, TConsoleProxyParams} from "./twrcon.js"; -import {twrConsoleRegistry} from "./twrconreg.js" +import {IConsole, logToCon} from "./twrcon.js"; +import {parseModOptions, IModOpts} from './twrmodutil.js' +import {IWasmMemoryAsync, twrWasmMemoryAsync} from "./twrwasmmem.js"; +import {twrWasmModuleCallAsync, TCallCAsync, TCallCImplAsync } from "./twrwasmcall.js" +import {TLibraryMessage, TLibraryProxyParams, twrLibraryInstanceRegistry} from "./twrlibrary.js" +import {twrEventQueueSend} from "./twreventqueue.js" +import {twrLibBuiltIns} from "./twrlibbuiltin.js" // class twrWasmModuleAsync consist of two parts: // twrWasmModuleAsync runs in the main JavaScript event loop // twrWasmModuleAsyncProxy runs in a WebWorker thread // - the wasm module is loaded by the webworker, and C calls into javascript are handed by proxy classes which call the 'main' class via a message -// - For example: -// twrConCharOut (exported from JavaScript to C) might call twrConsoleTerminalProxy.CharOut -// twrConsoleTerminalProxy.CharOut will send the message "term-charout". -// Ths message is received by twrWasmModuleAsync.processMsg(), which dispatches a call to twrConsoleTerminal.CharOut(). + +// IWasmModuleAsync is the Async version of IWasmModule +// Partial defines the deprecated module level memory access functions + + +export type TModuleMessage=[msgClass:"twrWasmModule", id:number, msgType:string, ...params:any[]]; + +export type TModAsyncMessage=TLibraryMessage|TModuleMessage; + +export interface IWasmModuleAsync extends Partial { + wasmMem: IWasmMemoryAsync; + callCInstance: twrWasmModuleCallAsync; + callC:TCallCAsync; + callCImpl:TCallCImplAsync; + eventQueueSend:twrEventQueueSend; + isTwrWasmModuleAsync:true; // to avoid circular references -- check if twrWasmModuleAsync without importing twrWasmModuleAsync + //TODO!! put these into IWasmModuleBase (some could be implemented in twrWasmModuleBase, but many have different implementations) + loadWasm: (pathToLoad:string)=>Promise; + postEvent:(eventID:number, ...params:number[])=>void; + fetchAndPutURL: (fnin:URL)=>Promise<[number, number]>; + divLog:(...params: string[])=>void; + log:(...params: string[])=>void; +} export type TModAsyncProxyStartupMsg = { urlToLoad: string, allProxyParams: IAllProxyParams, }; -// Interface for the error event -interface WorkerErrorEvent extends ErrorEvent { - filename: string; - lineno: number; - colno: number; - message: string; - error: Error | null; -} - interface ICallCPromise { - callCResolve: (value: unknown) => void; + callCResolve: (value: any) => void; callCReject: (reason?: any) => void; } - -export class twrWasmModuleAsync extends twrWasmModuleInJSMain { + +export class twrWasmModuleAsync implements IWasmModuleAsync { myWorker:Worker; - malloc:(size:number)=>Promise; loadWasmResolve?: (value: void) => void; loadWasmReject?: (reason?: any) => void; callCMap:Map; uniqueInt: number; initLW=false; - waitingcalls:twrWaitingCalls; - // d2dcanvas?:twrCanvas; - defined in twrWasmModuleInJSMain - // io:{[key:string]: IConsole}; - defined in twrWasmModuleInJSMain + io:{[key:string]: IConsole}; + ioNamesToID: {[key: string]: number}; + wasmMem!: IWasmMemoryAsync; + callCInstance!: twrWasmModuleCallAsync; + eventQueueSend:twrEventQueueSend=new twrEventQueueSend; + isTwrWasmModuleAsync:true=true; + + + // divLog is deprecated. Use IConsole.putStr or log + divLog:(...params: string[])=>void; + log:(...params: string[])=>void; + + // IWasmMemory + // These are deprecated, use wasmMem instead. + memory!:WebAssembly.Memory; + exports!:WebAssembly.Exports; + mem8!:Uint8Array; + mem32!:Uint32Array; + memD!:Float64Array; + stringToU8!:(sin:string, codePage?:number)=>Uint8Array; + copyString!:(buffer:number, buffer_size:number, sin:string, codePage?:number)=>void; + getLong!:(idx:number)=>number; + setLong!:(idx:number, value:number)=>void; + getDouble!:(idx:number)=>number; + setDouble!:(idx:number, value:number)=>void; + getShort!:(idx:number)=>number; + getString!:(strIndex:number, len?:number, codePage?:number)=>string; + getU8Arr!:(idx:number)=>Uint8Array; + getU32Arr!:(idx:number)=>Uint32Array; + + malloc!:(size:number)=>Promise; + free!:(size:number)=>Promise; + putString!:(sin:string, codePage?:number)=>Promise; + putU8!:(u8a:Uint8Array)=>Promise; + putArrayBuffer!:(ab:ArrayBuffer)=>Promise; constructor(opts?:IModOpts) { - super(opts); + + [this.io, this.ioNamesToID] = parseModOptions(opts); this.callCMap=new Map(); this.uniqueInt=1; - this.malloc=(size:number)=>{throw new Error("Error - un-init malloc called.")}; - if (!window.Worker) throw new Error("This browser doesn't support web workers."); const url=new URL('twrmodasyncproxy.js', import.meta.url); - //console.log("url=",url); this.myWorker = new Worker(url, {type: "module" }); - this.myWorker.onerror = (event: WorkerErrorEvent) => { + this.myWorker.onerror = (event: ErrorEvent) => { console.log("this.myWorker.onerror (undefined message typically means Worker failed to load)"); console.log("event.message: "+event.message) throw event; }; this.myWorker.onmessage= this.processMsg.bind(this); - this.waitingcalls=new twrWaitingCalls(); // handle's calls that cross the worker thread - main js thread boundary - + this.log=logToCon.bind(undefined, this.io.stdio); + this.divLog=this.log; } - // overrides base implementation async loadWasm(pathToLoad:string) { - if (this.initLW) throw new Error("twrWasmAsyncModule::loadWasm can only be called once per twrWasmAsyncModule instance"); + if (this.initLW) throw new Error("twrWasmModuleAsync::loadWasm can only be called once per instance"); this.initLW=true; + // load builtin libraries + await twrLibBuiltIns(); + return new Promise((resolve, reject)=>{ this.loadWasmResolve=resolve; this.loadWasmReject=reject; - this.malloc = (size:number) => { - return this.callCImpl("malloc", [size]) as Promise; + // libProxyParams will be everything needed to create Proxy versions of all twrLibraries + // libClassInstances has one entry per class, even if multiple instances of same class are registered (ie, interfaceName set) + let libProxyParams:TLibraryProxyParams[] = []; + for (let i=0; i{ + return new Promise((resolve, reject)=>{ const p:ICallCPromise={ callCResolve: resolve, callCReject: reject @@ -116,11 +160,31 @@ export class twrWasmModuleAsync extends twrWasmModuleInJSMain { this.myWorker.postMessage(['callC', this.uniqueInt, fname, cparams]); }); } + + // this implementation piggybacks of callCImpl -- it is essentially a specific version of callC + // instead of sending a message to the twrWasmModuleAsync thread (as callCImpl does), we post a malloc command + // in the eventQueue. This allows it to be processed by the twrWasmModuleAsync event loop. malloc was previously sent using callCImpl, but + // callCImpl uses postMessage, and the twrWasmModuleAsync thread will not process the callCImpl message while inside another callC, + // and malloc may be used by wasmMem.putXX functions, inside twrWasmLibrary derived classes, which are called from C, inside of a callC. + // + async mallocImpl(size:number) { + return new Promise((resolve, reject)=>{ + const p:ICallCPromise={ + callCResolve: resolve, + callCReject: reject + } + this.callCMap.set(++this.uniqueInt, p); + this.eventQueueSend.postMalloc(this.uniqueInt, size); + this.myWorker.postMessage(['tickleEventLoop']); + }); + } // the API user can call this to default to stdio - // or the API user can call keyDown on a particular + // or the API user can call keyDown on a particular console keyDown(ev:KeyboardEvent) { - keyDownUtil(this.io.stdio, ev); + if (!this.io.stdio) throw new Error("internal error - stdio not defined"); + if (!this.io.stdio.keyDown) throw new Error("stdio.keyDown not defined. Console must implemented IConsoleStreamIn.") + this.io.stdio.keyDown(ev); } // this function is deprecated and here for backward compatibility @@ -139,71 +203,120 @@ export class twrWasmModuleAsync extends twrWasmModuleInJSMain { throw new Error("keyDownCanvas is deprecated, but in any case should only be used with twr_iocanvas") } - processMsg(event: MessageEvent<[string, ...any[]]>) { - const [msgType, ...params]=event.data; - const d=params[0]; + // this.myWorker.onmessage = this.processMsg.bind(this); + async processMsg(event: MessageEvent) { + const msg=event.data; + const [msgClass, id]=msg; //console.log("twrWasmAsyncModule - got message: "+event.data) - switch (msgType) { - case "setmemory": - this.memory=d; - if (!this.memory) throw new Error("unexpected error - undefined memory"); - this.mem8 = new Uint8Array(this.memory.buffer); - this.mem32 = new Uint32Array(this.memory.buffer); - this.memD = new Float64Array(this.memory.buffer); - //console.log("memory set",this.mem8.length); - break; - - case "startupFail": - if (this.loadWasmReject) - this.loadWasmReject(d); - else - throw new Error("twrWasmAsyncModule.processMsg unexpected error (undefined loadWasmReject)"); - break; - - case "startupOkay": - - if (this.loadWasmResolve) - this.loadWasmResolve(undefined); - else - throw new Error("twrWasmAsyncModule.processMsg unexpected error (undefined loadWasmResolve)"); - break; - - case "callCFail": - { - const [id, returnCode]=params; - const p=this.callCMap.get(id); - if (!p) throw new Error("internal error"); - this.callCMap.delete(id); - if (p.callCReject) - p.callCReject(returnCode); - else - throw new Error("twrWasmAsyncModule.processMsg unexpected error (undefined callCReject)"); - } - break; - - case "callCOkay": - { - const [id, returnCode]=params; - const p=this.callCMap.get(id); - if (!p) throw new Error("internal error"); - this.callCMap.delete(id); - if (p.callCResolve) - p.callCResolve(returnCode); - else - throw new Error("twrWasmAsyncModule.processMsg unexpected error (undefined callCResolve)"); - break; + if (msgClass==="twrWasmModule") { + const [,, msgType, ...params]=msg; + + switch (msgType) { + case "setmemory": + this.memory=params[0]; + if (!this.memory) throw new Error("unexpected error - undefined memory"); + + this.wasmMem=new twrWasmMemoryAsync(this.memory, this.mallocImpl.bind(this), this.callCImpl.bind(this)); + this.callCInstance=new twrWasmModuleCallAsync(this.wasmMem, this.callCImpl.bind(this)); + + // backwards compatible + this.mem8 = this.wasmMem.mem8; + this.mem32 = this.wasmMem.mem32; + this.memD = this.wasmMem.memD; + this.stringToU8=this.wasmMem.stringToU8; + this.copyString=this.wasmMem.copyString; + this.getLong=this.wasmMem.getLong; + this.setLong=this.wasmMem.setLong; + this.getDouble=this.wasmMem.getDouble; + this.setDouble=this.wasmMem.setDouble; + this.getShort=this.wasmMem.getShort; + this.getString=this.wasmMem.getString; + this.getU8Arr=this.wasmMem.getU8Arr; + this.getU32Arr=this.wasmMem.getU32Arr; + + this.malloc=this.wasmMem.malloc; + this.free=this.wasmMem.free; + this.putString=this.wasmMem.putString; + this.putU8=this.wasmMem.putU8; + this.putArrayBuffer=this.wasmMem.putArrayBuffer; + break; + + case "startupFail": + const [returnCode]=params; + if (this.loadWasmReject) + this.loadWasmReject(returnCode); + else + throw new Error("twrWasmAsyncModule.processMsg unexpected error (undefined loadWasmReject)"); + break; + + case "startupOkay": + + if (this.loadWasmResolve) + this.loadWasmResolve(undefined); + else + throw new Error("twrWasmAsyncModule.processMsg unexpected error (undefined loadWasmResolve)"); + break; + + case "callCFail": + { + const [returnCode]=params; + const p=this.callCMap.get(id); + if (!p) throw new Error("internal error"); + this.callCMap.delete(id); + if (p.callCReject) + p.callCReject(returnCode); + else + throw new Error("twrWasmAsyncModule.processMsg unexpected error (undefined callCReject)"); + } + break; + + case "callCOkay": + { + const [returnCode]=params; + const p=this.callCMap.get(id); + if (!p) throw new Error("internal error"); + this.callCMap.delete(id); + if (p.callCResolve) + p.callCResolve(returnCode); + else + throw new Error("twrWasmAsyncModule.processMsg unexpected error (undefined callCResolve)"); + break; + } + + default: + throw new Error("internal error: "+msgType) } + } + + else if (msgClass==="twrLibrary") { + const lib=twrLibraryInstanceRegistry.getLibraryInstance(id); + const msgLib=msg as TLibraryMessage; + await lib.processMessageFromProxy(msg, this); + } + + else { + throw new Error("twrWasmAsyncModule - unknown and unexpected msgClass: "+msgClass); + } + } + + // given a url, load its contents, and stuff into Wasm memory similar to Unint8Array + async fetchAndPutURL(fnin:URL):Promise<[number, number]> { + + if (!(typeof fnin === 'object' && fnin instanceof URL)) + throw new Error("fetchAndPutURL param must be URL"); - default: - if (!this.waitingcalls) throw new Error ("internal error: this.waitingcalls undefined.") - if (this.waitingcalls.processMessage(msgType, d)) break; - // here if a console message - // console messages are an array with the first entry as the console ID - const con=twrConsoleRegistry.getConsole(d[0]); - if (con.processMessage(msgType, d, this)) break; - throw new Error("twrWasmAsyncModule - unknown and unexpected msgType: "+msgType); + try { + let response=await fetch(fnin); + let buffer = await response.arrayBuffer(); + let src = new Uint8Array(buffer); + let dest=await this.wasmMem.putU8(src); + return [dest, src.length]; + + } catch(err:any) { + console.log('fetchAndPutURL Error. URL: '+fnin+'\n' + err + (err.stack ? "\n" + err.stack : '')); + throw err; } } } diff --git a/source/twr-ts/twrmodasyncproxy.ts b/source/twr-ts/twrmodasyncproxy.ts index b0fe359c..736f1aff 100644 --- a/source/twr-ts/twrmodasyncproxy.ts +++ b/source/twr-ts/twrmodasyncproxy.ts @@ -1,48 +1,46 @@ import {TModAsyncProxyStartupMsg} from "./twrmodasync.js" -import {twrWasmModuleBase} from "./twrmodbase.js" -import {twrTimeEpochImpl} from "./twrdate.js" -import {twrTimeTmLocalImpl, twrUserLconvImpl, twrUserLanguageImpl, twrRegExpTest1252Impl,twrToLower1252Impl, twrToUpper1252Impl} from "./twrlocale.js" -import {twrStrcollImpl, twrUnicodeCodePointToCodePageImpl, twrCodePageToUnicodeCodePoint, twrGetDtnamesImpl} from "./twrlocale.js" -import {twrConsoleDivProxy} from "./twrcondiv.js"; -import {twrWaitingCallsProxy, TWaitingCallsProxyParams} from "./twrwaitingcalls.js"; -import {IConsoleProxy, TConsoleProxyParams} from "./twrcon.js" -import {twrConsoleCanvasProxy} from "./twrconcanvas.js"; -import {twrConsoleDebugProxy} from "./twrcondebug.js" -import {twrConsoleTerminalProxy} from "./twrconterm.js" -import {twrConsoleProxyRegistry} from "./twrconreg.js" - +import {twrWasmBase} from "./twrwasmbase.js" +import {TLibraryProxyParams, twrLibraryProxy, twrLibraryInstanceProxyRegistry} from "./twrlibrary.js" +import {twrEventQueueReceive} from "./twreventqueue.js" export interface IAllProxyParams { - conProxyParams: TConsoleProxyParams[], // everything needed to create matching IConsoleProxy for each IConsole and twrConsoleProxyRegistry - ioNamesToID: {[key:string]: number}, // name to id mappings for this module - waitingCallsProxyParams:TWaitingCallsProxyParams, + libProxyParams: TLibraryProxyParams[], + ioNamesToID: {[key:string]: number}, // name to id mappings for this module + eventQueueBuffer: SharedArrayBuffer } let mod:twrWasmModuleAsyncProxy; -self.onmessage = function(e) { +self.onmessage = function(e:MessageEvent<[string, ...params:any]>) { //console.log('twrworker.js: message received from main script: '+e.data); - if (e.data[0]=='startup') { - const params:TModAsyncProxyStartupMsg=e.data[1]; + const [msgType, ...params]=e.data; + + if (msgType==='startup') { + const [startMsg]=params as [TModAsyncProxyStartupMsg]; //console.log("Worker startup params:",params); - mod=new twrWasmModuleAsyncProxy(params.allProxyParams); + mod=new twrWasmModuleAsyncProxy(startMsg.allProxyParams); - mod.loadWasm(params.urlToLoad).then( ()=> { - postMessage(["startupOkay"]); + mod.loadWasm(startMsg.urlToLoad).then( ()=> { + postMessage(["twrWasmModule", undefined, "startupOkay"]); }).catch( (ex)=> { console.log(".catch: ", ex); - postMessage(["startupFail", ex]); + postMessage(["twrWasmModule", undefined, "startupFail", ex]); }); } - else if (e.data[0]=='callC') { - const [msg, id, funcName, cparams]=e.data; - mod.callCImpl(funcName, cparams).then( (rc)=> { - postMessage(["callCOkay", id, rc]); - }).catch(ex => { - console.log("exception in callC in 'twrmodasyncproxy.js': \n", e.data[1], e.data[2]); + else if (msgType==='callC') { + const [callcID, funcName, cparams]=params as [id:number, funcName:string, cparams?: (number | bigint)[]]; + try { + const rc=mod.wasmCall.callCImpl(funcName, cparams); + postMessage(["twrWasmModule", callcID, "callCOkay", rc]); + } + catch(ex: any) { + console.log("exception in callC in 'twrmodasyncproxy.js': \n", params); console.log(ex); - postMessage(["callCFail", id, ex]); - }); + postMessage(["twrWasmModule", callcID, "callCFail", ex]); + } + } + else if (msgType==='tickleEventLoop') { + mod.eventQueueReceive.processIncomingCommands(); } else { console.log("twrmodasyncproxy.js: unknown message: "+e); @@ -51,146 +49,58 @@ self.onmessage = function(e) { // ************************************************************************ -export class twrWasmModuleAsyncProxy extends twrWasmModuleBase { - malloc:(size:number)=>Promise; - imports:WebAssembly.ModuleImports; - ioNamesToID: {[key:string]: number}; // ioName to IConsole.id - cpTranslate:twrCodePageToUnicodeCodePoint; - - private getProxyInstance(params:TConsoleProxyParams): IConsoleProxy { - - const className=params[0]; - switch (className) { - case "twrConsoleDivProxy": - return new twrConsoleDivProxy(params); - - case "twrConsoleTerminalProxy": - return new twrConsoleTerminalProxy(params); - - case "twrConsoleDebugProxy": - return new twrConsoleDebugProxy(params); - - case "twrConsoleCanvasProxy": - return new twrConsoleCanvasProxy(params); - - default: - throw new Error("Unknown class name passed to getProxyClassConstructor: "+className); - } - } +export class twrWasmModuleAsyncProxy extends twrWasmBase { + allProxyParams:IAllProxyParams; + ioNamesToID: {[key: string]: number}; + libimports:WebAssembly.ModuleImports ={}; + eventQueueReceive: twrEventQueueReceive; constructor(allProxyParams:IAllProxyParams) { super(); - this.isAsyncProxy=true; - this.malloc=(size:number)=>{throw new Error("error - un-init malloc called")}; - this.cpTranslate=new twrCodePageToUnicodeCodePoint(); - - this.ioNamesToID=allProxyParams.ioNamesToID; - - // create IConsoleProxy versions of each IConsole - for (let i=0; i { - const con=twrConsoleProxyRegistry.getConsoleProxy(jsid); - const f=con[funcName] as (...args: any[]) => any; - if (!f) throw new Error(`Likely using an incorrect console type. jsid=${jsid}, funcName=${funcName}`); - return f.call(con, ...args); - } - - const conSetRange = (jsid:number, chars:number, start:number, len:number) => { - let values=[]; - for (let i=start; i { - conProxyCall("putStr", jsid, this.getString(chars), codePage); - } + this.allProxyParams=allProxyParams; + this.ioNamesToID=allProxyParams.ioNamesToID; + this.eventQueueReceive=new twrEventQueueReceive(this, allProxyParams.eventQueueBuffer); - const conGetProp = (jsid:number, pn:number) => { - const propName=this.getString(pn); - return conProxyCall("getProp", jsid, propName); - } - - const conDrawSeq = (jsid:number, ds:number) => { - conProxyCall("drawSeq", jsid, ds, this); + } + + async loadWasm(pathToLoad: string): Promise { + + // create twrLibraryProxy versions for each twrLibrary + for (let i=0; i { + const name=this.wasmMem.getString(nameIdx); + const id=this.ioNamesToID[name]; + if (id) + return id; + else + return -1; } - const twrGetConIDFromNameImpl = (nameIdx:number):number => { - const name=this.getString(nameIdx); - const id=this.ioNamesToID[name]; - if (id) - return id; - else - return -1; - } - - this.imports={ - twrTimeEpoch:twrTimeEpochImpl, - twrTimeTmLocal:twrTimeTmLocalImpl.bind(this), - twrUserLconv:twrUserLconvImpl.bind(this), - twrUserLanguage:twrUserLanguageImpl.bind(this), - twrRegExpTest1252:twrRegExpTest1252Impl.bind(this), - twrToLower1252:twrToLower1252Impl.bind(this), - twrToUpper1252:twrToUpper1252Impl.bind(this), - twrStrcoll:twrStrcollImpl.bind(this), - twrUnicodeCodePointToCodePage:twrUnicodeCodePointToCodePageImpl.bind(this), - twrCodePageToUnicodeCodePoint:this.cpTranslate.convert.bind(this.cpTranslate), - twrGetDtnames:twrGetDtnamesImpl.bind(this), - twrGetConIDFromName: twrGetConIDFromNameImpl, - - twrSleep:waitingCallsProxy.sleep.bind(waitingCallsProxy), - - twrConCharOut:conProxyCall.bind(null, "charOut"), - twrConCharIn:conProxyCall.bind(null, "charIn"), - twrSetFocus:conProxyCall.bind(null, "setFocus"), - - twrConGetProp:conGetProp, - twrConCls:conProxyCall.bind(null, "cls"), - twrConSetC32:conProxyCall.bind(null, "setC32"), - twrConSetReset:conProxyCall.bind(null, "setReset"), - twrConPoint:conProxyCall.bind(null, "point"), - twrConSetCursor:conProxyCall.bind(null, "setCursor"), - twrConSetColors:conProxyCall.bind(null, "setColors"), - twrConSetRange:conSetRange, - twrConPutStr:conPutStr, - - twrConDrawSeq:conDrawSeq, - twrConLoadImage: conProxyCall.bind(null, "loadImage"), - - twrSin:Math.sin, - twrCos:Math.cos, - twrTan: Math.tan, - twrFAbs: Math.abs, - twrACos: Math.acos, - twrASin: Math.asin, - twrATan: Math.atan, - twrExp: Math.exp, - twrFloor: Math.floor, - twrCeil: Math.ceil, - twrFMod: function(x:number, y:number) {return x%y}, - twrLog: Math.log, - twrPow: Math.pow, - twrSqrt: Math.sqrt, - twrTrunc: Math.trunc, - - twrDtoa: this.floatUtil.dtoa.bind(this.floatUtil), - twrToFixed: this.floatUtil.toFixed.bind(this.floatUtil), - twrToExponential: this.floatUtil.toExponential.bind(this.floatUtil), - twrAtod: this.floatUtil.atod.bind(this.floatUtil), - twrFcvtS: this.floatUtil.fcvtS.bind(this.floatUtil) + const imports:WebAssembly.ModuleImports = { + ...this.libimports, + twrConGetIDFromName: twrConGetIDFromNameImpl, } - } - - async loadWasm(pathToLoad:string) { - return super.loadWasm(pathToLoad, this.imports, this.ioNamesToID); + + await super.loadWasm(pathToLoad, imports); + + // SharedArrayBuffer required for twrWasmModuleAsync/twrWasmModuleAsyncProxy + // instanceof SharedArrayBuffer doesn't work when crossOriginIsolated not enable, and will cause a runtime error + // (don't check for instanceof SharedArrayBuffer, since it can cause an runtime error when SharedArrayBuffer does not exist) + if (this.wasmMem.memory.buffer instanceof ArrayBuffer) + throw new Error("twrWasmModuleAsync requires shared Memory. Add wasm-ld --shared-memory --no-check-features (see docs)"); + else + postMessage(["twrWasmModule", undefined, "setmemory", this.wasmMem.memory]); + + // init C runtime + const init=this.exports.twr_wasm_init as Function; + init(this.ioNamesToID.stdio, this.ioNamesToID.stderr, this.ioNamesToID.std2d==undefined?-1:this.ioNamesToID.std2d, this.wasmMem.mem8.length); } } diff --git a/source/twr-ts/twrmodbase.ts b/source/twr-ts/twrmodbase.ts deleted file mode 100644 index d1b5e4c4..00000000 --- a/source/twr-ts/twrmodbase.ts +++ /dev/null @@ -1,409 +0,0 @@ - -import {twrFloatUtil} from "./twrfloat.js"; -import {codePageUTF8, codePage1252, codePageASCII, to1252, toASCII} from "./twrlocale.js" -import {IConsole, IConsoleBase, IConsoleStream, IConsoleCanvas} from "./twrcon.js" - -export interface IModOpts { - stdio?: IConsoleStream&IConsoleBase, - d2dcanvas?: IConsoleCanvas&IConsoleBase, - io?: {[key:string]: IConsole}, - windim?:[number, number], - forecolor?:string, - backcolor?:string, - fontsize?:number, - imports?:{}, -} - -/*********************************************************************/ -/*********************************************************************/ -/*********************************************************************/ - -export abstract class twrWasmModuleBase { - memory?:WebAssembly.Memory; - mem8:Uint8Array; - mem32:Uint32Array; - memD:Float64Array; - abstract malloc:(size:number)=>Promise; - exports?:WebAssembly.Exports; - isAsyncProxy=false; - floatUtil:twrFloatUtil; - - constructor() { - this.mem8=new Uint8Array(); // avoid type errors - this.mem32=new Uint32Array(); // avoid type errors - this.memD=new Float64Array(); // avoid type errors - this.floatUtil=new twrFloatUtil(this); - //console.log("size of mem8 after constructor",this.mem8.length); - } - - /*********************************************************************/ - /*********************************************************************/ - - // overridden by twrWasmModuleAsync - async loadWasm(pathToLoad:string, imports:WebAssembly.ModuleImports, ioNamesToID:{[key:string]:number}) { - //console.log("fileToLoad",fileToLoad) - - let response; - try { - response=await fetch(pathToLoad); - } catch(err:any) { - console.log('loadWasm() failed to fetch: '+pathToLoad); - throw err; - } - - if (!response.ok) throw new Error("fetch response error on file '"+pathToLoad+"'\n"+response.statusText); - - try { - let wasmBytes = await response.arrayBuffer(); - - let instance = await WebAssembly.instantiate(wasmBytes, {env: imports}); - - this.exports=instance.instance.exports; - if (!this.exports) throw new Error("Unexpected error - undefined instance.exports"); - - if (this.memory) throw new Error ("unexpected error -- this.memory already set"); - this.memory=this.exports.memory as WebAssembly.Memory; - if (!this.memory) throw new Error("Unexpected error - undefined exports.memory"); - this.mem8 = new Uint8Array(this.memory.buffer); - this.mem32 = new Uint32Array(this.memory.buffer); - this.memD = new Float64Array(this.memory.buffer); - - // SharedArrayBuffer required for twrWasmModuleAsync/twrWasmModuleAsyncProxy - // instanceof SharedArrayBuffer doesn't work when crossOriginIsolated not enable, and will cause a runtime error - // (don't check for instanceof SharedArrayBuffer, since it can cause an runtime error when SharedArrayBuffer does not exist) - if (this.isAsyncProxy) { - if (this.memory.buffer instanceof ArrayBuffer) - console.log("twrWasmModuleAsync requires shared Memory. Add wasm-ld --shared-memory --no-check-features (see docs)"); - - postMessage(["setmemory",this.memory]); - } - - else { - // here if twrWasmModule because twrWasmModuleAsync overrides this function, and twrWasmModuleAsyncProxy was handled above - - if (!(this.memory.buffer instanceof ArrayBuffer)) - console.log("twrWasmModule does not require shared Memory. Okay to remove wasm-ld --shared-memory --no-check-features"); - } - - this.malloc=(size:number)=>{ - return new Promise(resolve => { - const m=this.exports!.malloc as (size:number)=>number; - resolve(m(size)); - }); - }; - - this.init(ioNamesToID); - - } catch(err:any) { - console.log('Wasm instantiate error: ' + err + (err.stack ? "\n" + err.stack : '')); - throw err; - } - } - - private init(ioNamesToID:{[key:string]:number}) { - const twrInit=this.exports!.twr_wasm_init as CallableFunction; - twrInit(ioNamesToID.stdio, ioNamesToID.stderr, ioNamesToID.std2d==undefined?-1:ioNamesToID.std2d, this.mem8.length); - } - - /* - * this is overridden by twrmodasync (although its worker side will call this version) - * - * callC takes an array where: - * the first entry is the name of the C function in the Wasm module to call (must be exported, typically via the --export clang flag) - * and the next entries are a variable number of arguments to pass to the C function, of type - * number - converted to int32 or float64 as appropriate - * string - converted to a an index (ptr) into a module Memory returned via stringToMem() - * URL - the file contents are loaded into module Memory via fetchAndPutURL(), and two C arguments are generated - index (pointer) to the memory, and length - * ArrayBuffer - the array is loaded into module memory via putArrayBuffer - */ - - async callC(params:[string, ...(string|number|bigint|ArrayBuffer|URL)[]]) { - const cparams=await this.preCallC(params); - let retval = await this.callCImpl(params[0], cparams); - await this.postCallC(cparams, params); - return retval; - } - - async callCImpl(fname:string, cparams:(number|bigint)[]=[]) { - if (!this.exports) throw new Error("this.exports undefined"); - if (!this.exports[fname]) throw new Error("callC: function '"+fname+"' not in export table. Use --export wasm-ld flag."); - - const f = this.exports[fname] as CallableFunction; - let cr=f(...cparams); - - return cr; - } - - // convert an array of arguments to numbers by stuffing contents into malloc'd Wasm memory - async preCallC(params:[string, ...(string|number|bigint|ArrayBuffer|URL)[]]) { - - if (!(params.constructor === Array)) throw new Error ("callC: params must be array, first arg is function name"); - if (params.length==0) throw new Error("callC: missing function name"); - - let cparams:(number|bigint)[]=[]; - let ci=0; - for (let i=1; i < params.length; i++) { - const p=params[i]; - switch (typeof p) { - case 'number': - case 'bigint': - cparams[ci++]=p; - break; - case 'string': - cparams[ci++]=await this.putString(p); - break; - case 'object': - if (p instanceof URL) { - const r=await this.fetchAndPutURL(p); - cparams[ci++]=r[0]; // mem index - cparams[ci++]=r[1]; // len - break; - } - else if (p instanceof ArrayBuffer) { - const r=await this.putArrayBuffer(p); - cparams[ci++]=r; // mem index - break; - } - default: - throw new Error ("callC: invalid object type passed in"); - } - } - - return cparams; - } - - // free the mallocs; copy array buffer data from malloc back to arraybuffer - async postCallC(cparams:(number|bigint)[], params:[string, ...(string|number|bigint|ArrayBuffer|URL)[]]) { - - let ci=0; - for (let i=1; i < params.length; i++) { - const p=params[i]; - switch (typeof p) { - case 'number': - case 'bigint': - ci++; - break; - - case 'string': - await this.callCImpl('free',[cparams[ci]]) - ci++; - break; - - case 'object': - if (p instanceof URL) { - await this.callCImpl('free',[cparams[ci]]) - ci=ci+2; - break; - } - else if (p instanceof ArrayBuffer) { - const u8=new Uint8Array(p); - const idx=cparams[ci] as number; - for (let j=0; j 0 (room for terminating 0): "+buffer_size); - - const ru8=this.stringToU8(sin, codePage); - - let i; - for (i=0; i= this.mem32.length) throw new Error("invalid index passed to getLong: "+idx+", this.mem32.length: "+this.mem32.length); - const long:number = this.mem32[idx32]; - return long; - } - - setLong(idx:number, value:number) { - const idx32 = Math.floor(idx / 4); - if (idx32 * 4 != idx) - throw new Error("setLong passed non long aligned address"); - if (idx32 < 0 || idx32 >= this.mem32.length-1) - throw new Error("invalid index passed to setLong: " + idx + ", this.mem32.length: " + this.mem32.length); - this.mem32[idx32]=value; - } - - getDouble(idx:number): number { - const idx64=Math.floor(idx/8); - if (idx64*8!=idx) throw new Error("getLong passed non Float64 aligned address") - const long:number = this.memD[idx64]; - return long; - } - - setDouble(idx:number, value:number) { - const idx64=Math.floor(idx/8); - if (idx64*8!=idx) throw new Error("setDouble passed non Float64 aligned address") - this.memD[idx64]=value; - } - - getShort(idx:number): number { - if (idx<0 || idx>= this.mem8.length) throw new Error("invalid index passed to getShort: "+idx); - const short:number = this.mem8[idx]+this.mem8[idx+1]*256; - return short; - } - - // get a string out of module memory - // null terminated, up until max of (optional) len bytes - // len may be longer than the number of characters, if characters are utf-8 encoded - getString(strIndex:number, len?:number, codePage=codePageUTF8): string { - if (strIndex<0 || strIndex >= this.mem8.length) throw new Error("invalid strIndex passed to getString: "+strIndex); - - if (len) { - if (len<0 || len+strIndex > this.mem8.length) throw new Error("invalid len passed to getString: "+len); - } - else { - len = this.mem8.indexOf(0, strIndex); - if (len==-1) throw new Error("string is not null terminated"); - len=len-strIndex; - } - - let encodeFormat; - if (codePage==codePageUTF8) encodeFormat='utf-8'; - else if (codePage==codePage1252) encodeFormat='windows-1252'; - else throw new Error("Unsupported codePage: "+codePage); - - const td=new TextDecoder(encodeFormat); - const u8todecode=new Uint8Array(this.mem8.buffer, strIndex, len); - - // chrome throws exception when using TextDecoder on SharedArrayBuffer - // BUT, instanceof SharedArrayBuffer doesn't work when crossOriginIsolated not enable, and will cause a runtime error, so don't check directly - if (this.mem8.buffer instanceof ArrayBuffer) { - const sout:string = td.decode(u8todecode); - return sout; - } - else { // must be SharedArrayBuffer - const regularArrayBuffer = new ArrayBuffer(len); - const regularUint8Array = new Uint8Array(regularArrayBuffer); - regularUint8Array.set(u8todecode); - const sout:string = td.decode(regularUint8Array); - return sout; - } - } - - // get a byte array out of module memory when passed in index to [size, dataptr] - getU8Arr(idx:number): Uint8Array { - if (idx<0 || idx>= this.mem8.length) throw new Error("invalid index passed to getU8: "+idx); - - const rv = new Uint32Array( (this.mem8.slice(idx, idx+8)).buffer ); - let size:number=rv[0]; - let dataptr:number=rv[1]; - - if (dataptr <0 || dataptr >= (this.mem8.length)) throw new Error("invalid idx.dataptr passed to getU8") - if (size <0 || size > (this.mem8.length-dataptr)) throw new Error("invalid idx.size passed to getU8") - - const u8=this.mem8.slice(dataptr, dataptr+size); - return u8; - } - - // get a int32 array out of module memory when passed in index to [size, dataptr] - getU32Arr(idx:number): Uint32Array { - if (idx<0 || idx>= this.mem8.length) throw new Error("invalid index passed to getU32: "+idx); - - const rv = new Uint32Array( (this.mem8.slice(idx, idx+8)).buffer ); - let size:number=rv[0]; - let dataptr:number=rv[1]; - - if (dataptr <0 || dataptr >= (this.mem8.length)) throw new Error("invalid idx.dataptr passed to getU32") - if (size <0 || size > (this.mem8.length-dataptr)) throw new Error("invalid idx.size passed to getU32") - - if (size%4!=0) throw new Error("idx.size is not an integer number of 32 bit words"); - - const u32 = new Uint32Array( (this.mem8.slice(dataptr, dataptr+size)).buffer ); - return u32; - } -} diff --git a/source/twr-ts/twrmodjsmain.ts b/source/twr-ts/twrmodjsmain.ts deleted file mode 100644 index 43da463f..00000000 --- a/source/twr-ts/twrmodjsmain.ts +++ /dev/null @@ -1,84 +0,0 @@ -// This class extends base to handle options when called in the main Java Script thread - -import {twrConsoleDiv} from "./twrcondiv.js" -import {IModOpts, twrWasmModuleBase} from "./twrmodbase.js" -import {twrConsoleCanvas} from "./twrconcanvas.js" -import {IConsole} from "./twrcon.js" -import {twrConsoleTerminal} from "./twrconterm.js" -import {codePageUTF32} from "./twrlocale.js" -import {twrConsoleDebug} from "./twrcondebug.js" - - -export abstract class twrWasmModuleInJSMain extends twrWasmModuleBase { - io:{[key:string]: IConsole}; - ioNamesToID: {[key: string]: number}; - - constructor(opts:IModOpts={}) { - super(); - if (typeof document === 'undefined') - throw new Error ("twrWasmModuleJSMain should only be created in JavaScript Main."); - - // io contains a mapping of names to IConsole - // stdio, stderr are required (but if they are not passed in, we will find defaults here) - // there can be an arbitrary number of IConsoles passed to a module for use by the module - if (opts.io) { - this.io=opts.io; - } - else { - this.io={}; - } - - if (!this.io.stdio) { - const eiodiv=document.getElementById("twr_iodiv") as HTMLDivElement; - const eiocanvas=document.getElementById("twr_iocanvas") as HTMLCanvasElement; - if (opts.stdio) { - this.io.stdio=opts.stdio; - } - else if (eiodiv) { - this.io.stdio=new twrConsoleDiv(eiodiv, {foreColor: opts.forecolor, backColor: opts.backcolor, fontSize: opts.fontsize}); - } - else if (eiocanvas) { - this.io.stdio=new twrConsoleTerminal(eiocanvas, { - foreColor: opts.forecolor, - backColor: opts.backcolor, - fontSize: opts.fontsize, - widthInChars: opts.windim?.[0], - heightInChars: opts.windim?.[1], - }); - } - else { - this.io.stdio=new twrConsoleDebug(); - console.log("Stdio console is not specified. Using twrConsoleDebug.") - } - } - - if (!this.io.stderr) { - this.io.stderr=new twrConsoleDebug(); - } - - if (!this.io.std2d) { - if (opts.d2dcanvas) { - this.io.std2d=opts.d2dcanvas; - } - else { - const ed2dcanvas=document.getElementById("twr_d2dcanvas") as HTMLCanvasElement; - if (ed2dcanvas) this.io.std2d=new twrConsoleCanvas(ed2dcanvas); - } - } - - // each module has a mapping of names to console.id - this.ioNamesToID={}; - Object.keys(this.io).forEach(key => { - this.ioNamesToID[key]=this.io[key].id; - }); - - } - - divLog(...params: string[]) { - for (var i = 0; i < params.length; i++) { - this.io.stdio.putStr!(params[i].toString()); - this.io.stdio.charOut!(32, codePageUTF32); // space - } - this.io.stdio.charOut!(10, codePageUTF32); - } -} \ No newline at end of file diff --git a/source/twr-ts/twrmodutil.ts b/source/twr-ts/twrmodutil.ts new file mode 100644 index 00000000..6e5b582b --- /dev/null +++ b/source/twr-ts/twrmodutil.ts @@ -0,0 +1,88 @@ +// This class extends base to handle options when called in the main Java Script thread + +import {IConsole, IConsoleBase, IConsoleStreamOut, IConsoleCanvas} from "./twrcon.js" +import {twrConsoleDiv} from "./twrcondiv.js" +import {twrConsoleCanvas} from "./twrconcanvas.js" +import {twrConsoleTerminal} from "./twrconterm.js" +import {twrConsoleDebug} from "./twrcondebug.js" +import {codePageUTF8} from "./twrliblocale.js" + + +export interface IModOpts { + stdio?: IConsoleStreamOut&IConsoleBase, + d2dcanvas?: IConsoleCanvas&IConsoleBase, + io?: {[key:string]: IConsole}, + windim?:[number, number], + forecolor?:string, + backcolor?:string, + fontsize?:number, + imports?:{}, +} + +export type IParseModOptsResult = [{[key:string]: IConsole}, {[key: string]: number}]; + +export function parseModOptions(opts:IModOpts={}):IParseModOptsResult { + let io:{[key:string]: IConsole}; + let ioNamesToID: {[key: string]: number}; + + if (typeof document === 'undefined') + throw new Error ("twrWasmModuleJSMain should only be created in JavaScript Main."); + + // io contains a mapping of names to IConsole + // stdio, stderr are required (but if they are not passed in, we will find defaults here) + // there can be an arbitrary number of IConsoles passed to a module for use by the module + if (opts.io) { + io=opts.io; + } + else { + io={}; + } + + if (!io.stdio) { + const eiodiv=document.getElementById("twr_iodiv") as HTMLDivElement; + const eiocanvas=document.getElementById("twr_iocanvas") as HTMLCanvasElement; + if (opts.stdio) { + io.stdio=opts.stdio; + } + else if (eiodiv) { + io.stdio=new twrConsoleDiv(eiodiv, {foreColor: opts.forecolor, backColor: opts.backcolor, fontSize: opts.fontsize}); + } + else if (eiocanvas) { + io.stdio=new twrConsoleTerminal(eiocanvas, { + foreColor: opts.forecolor, + backColor: opts.backcolor, + fontSize: opts.fontsize, + widthInChars: opts.windim?.[0], + heightInChars: opts.windim?.[1], + }); + } + else { + io.stdio=new twrConsoleDebug(); + console.log("Stdio console is not specified. Using twrConsoleDebug.") + } + } + + if (!io.stderr) { + io.stderr=new twrConsoleDebug(); + } + + if (!io.std2d) { + if (opts.d2dcanvas) { + io.std2d=opts.d2dcanvas; + } + else { + const ed2dcanvas=document.getElementById("twr_d2dcanvas") as HTMLCanvasElement; + if (ed2dcanvas) io.std2d=new twrConsoleCanvas(ed2dcanvas); + } + } + + // each module has a mapping of names to console.id + ioNamesToID={}; + Object.keys(io).forEach(key => { + ioNamesToID[key]=io[key].id; + }); + + return [io, ioNamesToID]; + +} + diff --git a/source/twr-ts/twrsignal.ts b/source/twr-ts/twrsignal.ts index cd055a31..2c8d2cd7 100644 --- a/source/twr-ts/twrsignal.ts +++ b/source/twr-ts/twrsignal.ts @@ -9,39 +9,39 @@ enum twrSignalState { }; export class twrSignal { - sharedArray:SharedArrayBuffer; - buf:Int32Array; + saBuffer:SharedArrayBuffer; + i32Array:Int32Array; constructor (sa?:SharedArrayBuffer) { if (typeof window !== 'undefined') { // this check only works if window valid if (!crossOriginIsolated && !(window.location.protocol === 'file:')) throw new Error("twrSignal constructor, crossOriginIsolated="+crossOriginIsolated+". See SharedArrayBuffer docs."); } - if (sa) this.sharedArray=sa; - else this.sharedArray=new SharedArrayBuffer(4); - this.buf=new Int32Array(this.sharedArray); - this.buf[0]=twrSignalState.WAITING; + if (sa) this.saBuffer=sa; + else this.saBuffer=new SharedArrayBuffer(4); + this.i32Array=new Int32Array(this.saBuffer); + this.i32Array[0]=twrSignalState.WAITING; } signal() { - this.buf[0]=twrSignalState.SIGNALED; + this.i32Array[0]=twrSignalState.SIGNALED; //console.log("about to signal"); - Atomics.notify(this.buf, 0); + Atomics.notify(this.i32Array, 0); } wait() { - if (this.buf[0]==twrSignalState.WAITING) { + if (this.i32Array[0]==twrSignalState.WAITING) { //console.log("waiting..."); - Atomics.wait(this.buf, 0, twrSignalState.WAITING); + Atomics.wait(this.i32Array, 0, twrSignalState.WAITING); //console.log("released..."); } } isSignaled():boolean { - return this.buf[0]==twrSignalState.SIGNALED; + return this.i32Array[0]==twrSignalState.SIGNALED; } reset() { - this.buf[0]=twrSignalState.WAITING; + this.i32Array[0]=twrSignalState.WAITING; } } diff --git a/source/twr-ts/twrwaitingcalls.ts b/source/twr-ts/twrwaitingcalls.ts deleted file mode 100644 index 47655413..00000000 --- a/source/twr-ts/twrwaitingcalls.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { twrSignal } from "./twrsignal.js"; - -// These classes are used to proxy a call across the worker thread - main thread boundary and wait for the result - -export type TWaitingCallsProxyParams = [SharedArrayBuffer, SharedArrayBuffer]; // twrSignal, arguments - -// This class is used in the Main JS thread -export class twrWaitingCalls { - callCompleteSignal:twrSignal; - parameters:Uint32Array; - - constructor() { - this.callCompleteSignal=new twrSignal(); - this.parameters=new Uint32Array(new SharedArrayBuffer(4)); - } - - private startSleep(ms:number) { - - setTimeout(()=>{ - this.callCompleteSignal.signal(); - }, ms); - - } - - getProxyParams():TWaitingCallsProxyParams { - return [this.callCompleteSignal.sharedArray, this.parameters.buffer as SharedArrayBuffer]; - } - - processMessage(msgType:string, data:any[]):boolean { - switch (msgType) { - case "sleep": - const [ms] = data; - this.startSleep(ms); - break; - - default: - return false; - } - - return true; - } - -} - -// This class is used in the worker thread -export class twrWaitingCallsProxy { - callCompleteSignal:twrSignal; - parameters:Uint32Array; - - constructor(params:TWaitingCallsProxyParams) { - this.callCompleteSignal=new twrSignal(params[0]); - this.parameters=new Uint32Array(params[1]); - } - - sleep(ms:number) { - this.callCompleteSignal.reset(); - postMessage(["sleep", [ms]]); - this.callCompleteSignal.wait(); - } - -} diff --git a/source/twr-ts/twrwasmbase.ts b/source/twr-ts/twrwasmbase.ts new file mode 100644 index 00000000..671647cb --- /dev/null +++ b/source/twr-ts/twrwasmbase.ts @@ -0,0 +1,69 @@ +import {IWasmMemory, twrWasmMemory} from './twrwasmmem.js' +import {twrWasmCall} from "./twrwasmcall.js" +import { twrEventQueueReceive } from './twreventqueue.js'; + + +// twrWasmBase is the common code for any twrWasmModuleXXX that loads a .wasm file into its thread. +// This is twrWasmModule and twrWasmModuleAsyncProxy. +// twrWasmBase implements loadWasm (which is passed an import list), as well as containing the classes +// twrWasmMemory (to access wasm memory) and twrWasmCall (to call wasm exports) + +export type TOnEventCallback = (eventID:number, ...args:number[])=>void; + +export class twrWasmBase { + exports!:WebAssembly.Exports; + wasmMem!: IWasmMemory; + wasmCall!: twrWasmCall; + callC!:twrWasmCall["callC"]; + + /*********************************************************************/ + + private getImports(imports:WebAssembly.ModuleImports) { + return { + ...imports, + twr_register_callback:this.registerCallback.bind(this) + } + } + + async loadWasm(pathToLoad:string, imports:WebAssembly.ModuleImports) { + let response; + try { + response=await fetch(pathToLoad); + if (!response.ok) throw new Error("Fetch response error on file '"+pathToLoad+"'\n"+response.statusText); + } catch(err:any) { + console.log('loadWasm() failed to fetch: '+pathToLoad); + throw err; + } + + let instance; + try { + const wasmBytes = await response.arrayBuffer(); + instance = await WebAssembly.instantiate(wasmBytes, {env: this.getImports(imports)}); + } catch(err:any) { + console.log('Wasm instantiate error: ' + err + (err.stack ? "\n" + err.stack : '')); + throw err; + } + + if (this.exports) throw new Error ("Unexpected error -- this.exports already set"); + if (!instance.instance.exports) throw new Error("Unexpected error - undefined instance.exports"); + this.exports=instance.instance.exports; + + const memory=this.exports.memory as WebAssembly.Memory; + if (!memory) throw new Error("Unexpected error - undefined exports.memory"); + + const malloc=this.exports.malloc as (size:number)=>number; + const free=this.exports.free as (size:number)=>number; + this.wasmMem=new twrWasmMemory(memory, free, malloc); + this.wasmCall=new twrWasmCall(this.wasmMem, this.exports); + this.callC=this.wasmCall.callC.bind(this.wasmCall); + } + + + //see twrWasmModule.constructor - imports - twr_register_callback:this.registerCallback.bind(this), + registerCallback(funcNameIdx:number) { + const funcName=this.wasmMem.getString(funcNameIdx); + const onEventCallback = this.exports[funcName] as TOnEventCallback; + return twrEventQueueReceive.registerCallback(funcName, onEventCallback); + } + +} diff --git a/source/twr-ts/twrwasmcall.ts b/source/twr-ts/twrwasmcall.ts new file mode 100644 index 00000000..5befe843 --- /dev/null +++ b/source/twr-ts/twrwasmcall.ts @@ -0,0 +1,215 @@ +/* + * callC takes an array where: + * the first entry is the name of the C function in the Wasm module to call (must be exported, typically via the --export clang flag) + * and the next entries are a variable number of arguments to pass to the C function, of type + * number - converted to int32 or float64 as appropriate + * bigint - converted to int64 + * string - converted to a an index (ptr) into a module Memory returned via stringToMem() + * ArrayBuffer - the array is loaded into module memory via putArrayBuffer + */ + +import {twrWasmMemory, twrWasmMemoryAsync} from "./twrwasmmem"; + + +export class twrWasmCall { + exports: WebAssembly.Exports; + mem: twrWasmMemory; + + constructor(mem:twrWasmMemory, exports:WebAssembly.Exports) { + if (!exports) throw new Error("WebAssembly.Exports undefined"); + + this.exports=exports; + this.mem=mem; + } + + callCImpl(fname:string, cparams:(number|bigint)[]=[]) { + if (!this.exports[fname]) throw new Error("callC: function '"+fname+"' not in export table. Use --export wasm-ld flag."); + const f = this.exports[fname] as Function; + let cr=f(...cparams); + + return cr; + } + + callC(params:[string, ...(string|number|bigint|ArrayBuffer)[]]) { + const cparams=this.preCallC(params); + let retval = this.callCImpl(params[0], cparams); + this.postCallC(cparams, params); + return retval; + } + + // convert an array of arguments to numbers by stuffing contents into malloc'd Wasm memory + preCallC(params:[string, ...(string|number|bigint|ArrayBuffer)[]]) { + + if (!(params.constructor === Array)) throw new Error ("callC: params must be array, first arg is function name"); + if (params.length==0) throw new Error("callC: missing function name"); + + let cparams:(number|bigint)[]=[]; + let ci=0; + for (let i=1; i < params.length; i++) { + const p=params[i]; + switch (typeof p) { + case 'number': + case 'bigint': + cparams[ci++]=p; + break; + case 'string': + cparams[ci++]=this.mem.putString(p); + break; + case 'object': + if (p instanceof URL) { + throw new Error("URL arg in callC is no longer supported directly. use module.fetchAndPutURL"); + } + else if (p instanceof ArrayBuffer) { + const r=this.mem.putArrayBuffer(p); + cparams[ci++]=r; // mem index + break; + } + default: + throw new Error ("callC: invalid object type passed in"); + } + } + + return cparams; + } + + // free the mallocs; copy array buffer data from malloc back to arraybuffer + postCallC(cparams:(number|bigint)[], params:[string, ...(string|number|bigint|ArrayBuffer)[]]) { + + let ci=0; + for (let i=1; i < params.length; i++) { + const p=params[i]; + switch (typeof p) { + case 'number': + case 'bigint': + ci++; + break; + + case 'string': + this.callCImpl('free',[cparams[ci]]) + ci++; + break; + + case 'object': + if (p instanceof URL) { + //this.callCImpl('free',[cparams[ci]]) + //ci=ci+2; + throw new Error("internal error"); + } + else if (p instanceof ArrayBuffer) { + const u8=new Uint8Array(p); + const idx=cparams[ci] as number; + for (let j=0; jPromise; +export type TCallCAsync=(params:[string, ...(string|number|bigint|ArrayBuffer)[]])=>Promise; + +export class twrWasmModuleCallAsync { + mem: twrWasmMemoryAsync; + callCImpl: TCallCImplAsync; + + constructor(mem:twrWasmMemoryAsync, callCImpl:TCallCImplAsync) { + this.mem=mem; + this.callCImpl=callCImpl; + } + + // convert an array of arguments to numbers by stuffing contents into malloc'd Wasm memory + async preCallC(params:[string, ...(string|number|bigint|ArrayBuffer)[]]) { + + if (!(params.constructor === Array)) throw new Error ("callC: params must be array, first arg is function name"); + if (params.length==0) throw new Error("callC: missing function name"); + + let cparams:(number|bigint)[]=[]; + let ci=0; + for (let i=1; i < params.length; i++) { + const p=params[i]; + switch (typeof p) { + case 'number': + case 'bigint': + cparams[ci++]=p; + break; + case 'string': + cparams[ci++]=await this.mem.putString(p); + break; + case 'object': + if (p instanceof URL) { + throw new Error("URL arg in callC is no longer supported directly. use module.fetchAndPutURL"); + } + else if (p instanceof ArrayBuffer) { + const r=await this.mem.putArrayBuffer(p); + cparams[ci++]=r; // mem index + break; + } + default: + throw new Error ("callC: invalid object type passed in"); + } + } + + return cparams; + } + + // free the mallocs; copy array buffer data from malloc back to arraybuffer + async postCallC(cparams:(number|bigint)[], params:[string, ...(string|number|bigint|ArrayBuffer)[]]) { + + let ci=0; + for (let i=1; i < params.length; i++) { + const p=params[i]; + switch (typeof p) { + case 'number': + case 'bigint': + ci++; + break; + + case 'string': + await this.callCImpl('free',[cparams[ci]]) + ci++; + break; + + case 'object': + if (p instanceof URL) { + //await this.callCImpl('free',[cparams[ci]]) + //ci=ci+2; + throw new Error("internal error"); + } + else if (p instanceof ArrayBuffer) { + const u8=new Uint8Array(p); + const idx=cparams[ci] as number; + for (let j=0; jnumber; + free:(size:number)=>void; + putString(sin:string, codePage?:number):number; + putU8(u8a:Uint8Array):number; + putArrayBuffer(ab:ArrayBuffer):number; +} + +// IWasmMemoryAsync must be used from an async function since await is needed +export interface IWasmMemoryAsync extends IWasmMemoryBase { + malloc:(size:number)=>Promise; + free:(size:number)=>Promise; + putString(sin:string, codePage?:number):Promise; + putU8(u8a:Uint8Array):Promise; + putArrayBuffer(ab:ArrayBuffer):Promise; +} + +/**********************************************************************************************/ +/**********************************************************************************************/ +/**********************************************************************************************/ +/**********************************************************************************************/ + +export class twrWasmMemoryBase implements IWasmMemoryBase { + memory:WebAssembly.Memory; + mem8:Uint8Array; + mem16:Uint16Array; + mem32:Uint32Array; + memF:Float32Array; + memD:Float64Array; + + constructor(memory:WebAssembly.Memory) { + this.memory=memory; + this.mem8 = new Uint8Array(memory.buffer); + this.mem16 = new Uint16Array(memory.buffer); + this.mem32 = new Uint32Array(memory.buffer); + this.memF = new Float32Array(memory.buffer); + this.memD = new Float64Array(memory.buffer); + } + + // convert a Javascript string into byte sequence that encodes the string using UTF8, or the requested codePage + stringToU8(sin:string, codePage=codePageUTF8) { + + let ru8:Uint8Array; + if (codePage==codePageUTF8) { + const encoder = new TextEncoder(); + ru8=encoder.encode(sin); + } + else if (codePage==codePage1252) { + ru8=new Uint8Array(sin.length); + for (let i = 0; i < sin.length; i++) { + ru8[i]=to1252(sin[i]); + } + } + else if (codePage==codePageASCII) { + ru8=new Uint8Array(sin.length); + for (let i = 0; i < sin.length; i++) { + const r=toASCII(sin[i]); + ru8[i]=r; + } + } + else { + throw new Error("unknown codePage: "+codePage); + } + + return ru8; + } + + // copy a string into existing buffer in the webassembly module memory as utf8 (or specified codePage) + // result always null terminated + copyString(buffer:number, buffer_size:number, sin:string, codePage=codePageUTF8):void { + if (buffer_size<1) throw new Error("copyString buffer_size must have length > 0 (room for terminating 0): "+buffer_size); + + const ru8=this.stringToU8(sin, codePage); + + let i; + for (i=0; i= this.mem32.length) throw new Error("invalid index passed to getLong: "+idx+", this.mem32.length: "+this.mem32.length); + const long:number = this.mem32[idx32]; + return long; + } + + setLong(idx:number, value:number) { + const idx32 = Math.floor(idx / 4); + if (idx32 * 4 != idx) + throw new Error("setLong passed non long aligned address"); + if (idx32 < 0 || idx32 >= this.mem32.length-1) + throw new Error("invalid index passed to setLong: " + idx + ", this.mem32.length: " + this.mem32.length); + this.mem32[idx32]=value; + } + + getDouble(idx:number): number { + const idx64=Math.floor(idx/8); + if (idx64*8!=idx) throw new Error("getLong passed non Float64 aligned address") + const long:number = this.memD[idx64]; + return long; + } + + setDouble(idx:number, value:number) { + const idx64=Math.floor(idx/8); + if (idx64*8!=idx) throw new Error("setDouble passed non Float64 aligned address") + this.memD[idx64]=value; + } + + getShort(idx:number): number { + if (idx<0 || idx>= this.mem8.length) throw new Error("invalid index passed to getShort: "+idx); + const short:number = this.mem8[idx]+this.mem8[idx+1]*256; + return short; + } + + // get a string out of module memory + // null terminated, up until max of (optional) len bytes + // len may be longer than the number of characters, if characters are utf-8 encoded + getString(strIndex:number, len?:number, codePage=codePageUTF8): string { + if (strIndex<0 || strIndex >= this.mem8.length) throw new Error("invalid strIndex passed to getString: "+strIndex); + + if (len) { + if (len<0 || len+strIndex > this.mem8.length) throw new Error("invalid len passed to getString: "+len); + } + else { + len = this.mem8.indexOf(0, strIndex); + if (len==-1) throw new Error("string is not null terminated"); + len=len-strIndex; + } + + let encodeFormat; + if (codePage==codePageUTF8) encodeFormat='utf-8'; + else if (codePage==codePage1252) encodeFormat='windows-1252'; + else throw new Error("Unsupported codePage: "+codePage); + + const td=new TextDecoder(encodeFormat); + const u8todecode=new Uint8Array(this.mem8.buffer, strIndex, len); + + // chrome throws exception when using TextDecoder on SharedArrayBuffer + // BUT, instanceof SharedArrayBuffer doesn't work when crossOriginIsolated not enable, and will cause a runtime error, so don't check directly + if (this.mem8.buffer instanceof ArrayBuffer) { + const sout:string = td.decode(u8todecode); + return sout; + } + else { // must be SharedArrayBuffer + const regularArrayBuffer = new ArrayBuffer(len); + const regularUint8Array = new Uint8Array(regularArrayBuffer); + regularUint8Array.set(u8todecode); + const sout:string = td.decode(regularUint8Array); + return sout; + } + } + + // get a byte array out of module memory when passed in index to [size, dataptr] + getU8Arr(idx:number): Uint8Array { + if (idx<0 || idx>= this.mem8.length) throw new Error("invalid index passed to getU8: "+idx); + + const rv = new Uint32Array( (this.mem8.slice(idx, idx+8)).buffer ); + let size:number=rv[0]; + let dataptr:number=rv[1]; + + if (dataptr <0 || dataptr >= (this.mem8.length)) throw new Error("invalid idx.dataptr passed to getU8") + if (size <0 || size > (this.mem8.length-dataptr)) throw new Error("invalid idx.size passed to getU8") + + const u8=this.mem8.slice(dataptr, dataptr+size); + return u8; + } + + // get a int32 array out of module memory when passed in index to [size, dataptr] + getU32Arr(idx:number): Uint32Array { + if (idx<0 || idx>= this.mem8.length) throw new Error("invalid index passed to getU32: "+idx); + + const rv = new Uint32Array( (this.mem8.slice(idx, idx+8)).buffer ); + let size:number=rv[0]; + let dataptr:number=rv[1]; + + if (dataptr <0 || dataptr >= (this.mem8.length)) throw new Error("invalid idx.dataptr passed to getU32") + if (size <0 || size > (this.mem8.length-dataptr)) throw new Error("invalid idx.size passed to getU32") + + if (size%4!=0) throw new Error("idx.size is not an integer number of 32 bit words"); + + const u32 = new Uint32Array( (this.mem8.slice(dataptr, dataptr+size)).buffer ); + return u32; + } +} + +/**********************************************************************************************/ +/**********************************************************************************************/ +/**********************************************************************************************/ +/**********************************************************************************************/ + +export class twrWasmMemory extends twrWasmMemoryBase implements IWasmMemory { + malloc:(size:number)=>number; + free:(size:number)=>void; + + constructor(memory:WebAssembly.Memory, free:(size:number)=>void, malloc:(size:number)=>number) { + super(memory); + this.free=free; + this.malloc=malloc; + } + + // allocate and copy a string into the webassembly module memory as utf8 (or the specified codePage) + putString(sin:string, codePage=codePageUTF8) { + const ru8=this.stringToU8(sin, codePage); + const strIndex:number=this.malloc(ru8.length+1); + this.mem8.set(ru8, strIndex); + this.mem8[strIndex+ru8.length]=0; + + return strIndex; + } + + // allocate and copy a Uint8Array into Wasm mod memory + putU8(u8a:Uint8Array) { + let dest:number=this.malloc(u8a.length); + this.mem8.set(u8a, dest); + return dest; + } + + putArrayBuffer(ab:ArrayBuffer) { + const u8=new Uint8Array(ab); + return this.putU8(u8); + } +} + +/**********************************************************************************************/ +/**********************************************************************************************/ +/**********************************************************************************************/ +/**********************************************************************************************/ + +export class twrWasmMemoryAsync extends twrWasmMemoryBase implements IWasmMemoryAsync { + malloc:(size:number)=>Promise; + free:(size:number)=>Promise; + + constructor(memory:WebAssembly.Memory, mallocImpl:(size:number)=>Promise, callCImpl:(funcName:string, any:[...any])=>Promise) { + super(memory); + this.free = (size:number) => { + return callCImpl("free", [size]) as Promise; + } + this.malloc = mallocImpl; + } + + // allocate and copy a string into the webassembly module memory as utf8 (or the specified codePage) + async putString(sin:string, codePage=codePageUTF8) { + const ru8=this.stringToU8(sin, codePage); + const strIndex:number=await this.malloc(ru8.length+1); + this.mem8.set(ru8, strIndex); + this.mem8[strIndex+ru8.length]=0; + + return strIndex; + } + + // allocate and copy a Uint8Array into Wasm mod memory + async putU8(u8a:Uint8Array) { + let dest:number=await this.malloc(u8a.length); + this.mem8.set(u8a, dest); + return dest; + } + + async putArrayBuffer(ab:ArrayBuffer) { + const u8=new Uint8Array(ab); + return this.putU8(u8); + } +} + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/twr-wasm-gcc.code-workspace b/twr-wasm-gcc.code-workspace index 7ac90d4d..d2242af8 100644 --- a/twr-wasm-gcc.code-workspace +++ b/twr-wasm-gcc.code-workspace @@ -5,6 +5,9 @@ } ], "settings": { + "editor.tabSize": 3, + "editor.insertSpaces": true, + "editor.detectIndentation": false, "git.openRepositoryInParentFolders": "prompt", "files.associations": { "stdbool.h": "c", diff --git a/twr-wasm.code-workspace b/twr-wasm.code-workspace index aefc5508..d492966f 100644 --- a/twr-wasm.code-workspace +++ b/twr-wasm.code-workspace @@ -7,99 +7,101 @@ "settings": { "git.openRepositoryInParentFolders": "prompt", "files.associations": { - "*.cpp": "cpp", - "stdbool.h": "c", - "io.h": "c", - "stdio.h": "c", - "assert.h": "c", - "crtdefs.h": "c", - "corecrt.h": "c", - "stdint.h": "c", - "stdarg.h": "c", - "stddef.h": "c", - "twr-io.h": "c", - "twr-crt.h": "c", - "string.h": "c", - "math.h": "c", - "stdlib.h": "c", - "twr-wasm.h": "c", - "stack.h": "c", - "twr-draw2d.h": "c", - "winemu.h": "c", - "*.xx": "c", - "s21_math.h": "c", - "random": "cpp", - "_kiss_fft_guts.h": "c", - "kiss_fft.h": "c", - "ctype.h": "c", - "twr-jsimports.h": "c", - "__split_buffer": "cpp", - "deque": "cpp", - "string": "cpp", - "vector": "cpp", - "queue": "c", - "stack": "c", - "locale.h": "c", - "__locale": "c", - "ostream": "cpp", - "__hash_table": "c", - "array": "c", - "bitset": "c", - "initializer_list": "c", - "string_view": "c", - "unordered_map": "c", - "sstream": "c", - "complex": "c", - "forward_list": "c", - "__bit_reference": "cpp", - "span": "cpp", - "__config": "c", - "__node_handle": "c", - "__threading_support": "c", - "__verbose_abort": "c", - "cctype": "c", - "charconv": "c", - "cinttypes": "c", - "clocale": "c", - "cmath": "c", - "cstdarg": "c", - "cstddef": "c", - "cstdint": "c", - "cstdio": "c", - "cstdlib": "c", - "cstring": "c", - "ctime": "c", - "cwchar": "c", - "cwctype": "c", - "execution": "c", - "iomanip": "c", - "ios": "c", - "iosfwd": "c", - "iostream": "c", - "istream": "c", - "limits": "c", - "locale": "c", - "mutex": "c", - "new": "c", - "optional": "c", - "ratio": "c", - "regex": "c", - "stdexcept": "c", - "streambuf": "c", - "tuple": "c", - "typeinfo": "c", - "variant": "c", - "limits.h": "c", - "time.h": "c", - "_stdtypes.h": "c", - "__memory": "c", - "__tree": "cpp", - "map": "cpp", - "codecvt": "cpp", - "cuchar": "cpp", - "uchar.h": "c", - "type_traits": "c" - }, + "*.cpp": "cpp", + "stdbool.h": "c", + "io.h": "c", + "stdio.h": "c", + "assert.h": "c", + "crtdefs.h": "c", + "corecrt.h": "c", + "stdint.h": "c", + "stdarg.h": "c", + "stddef.h": "c", + "twr-io.h": "c", + "twr-crt.h": "c", + "string.h": "c", + "math.h": "c", + "stdlib.h": "c", + "twr-wasm.h": "c", + "stack.h": "c", + "twr-draw2d.h": "c", + "winemu.h": "c", + "*.xx": "c", + "s21_math.h": "c", + "random": "cpp", + "_kiss_fft_guts.h": "c", + "kiss_fft.h": "c", + "ctype.h": "c", + "twr-jsimports.h": "c", + "__split_buffer": "cpp", + "deque": "cpp", + "string": "cpp", + "vector": "cpp", + "queue": "c", + "stack": "c", + "locale.h": "c", + "__locale": "c", + "ostream": "cpp", + "__hash_table": "c", + "array": "c", + "bitset": "c", + "initializer_list": "c", + "string_view": "c", + "unordered_map": "c", + "sstream": "c", + "complex": "c", + "forward_list": "c", + "__bit_reference": "cpp", + "span": "cpp", + "__config": "c", + "__node_handle": "c", + "__threading_support": "c", + "__verbose_abort": "c", + "cctype": "c", + "charconv": "c", + "cinttypes": "c", + "clocale": "c", + "cmath": "c", + "cstdarg": "c", + "cstddef": "c", + "cstdint": "c", + "cstdio": "c", + "cstdlib": "c", + "cstring": "c", + "ctime": "c", + "cwchar": "c", + "cwctype": "c", + "execution": "c", + "iomanip": "c", + "ios": "c", + "iosfwd": "c", + "iostream": "c", + "istream": "c", + "limits": "c", + "locale": "c", + "mutex": "c", + "new": "c", + "optional": "c", + "ratio": "c", + "regex": "c", + "stdexcept": "c", + "streambuf": "c", + "tuple": "c", + "typeinfo": "c", + "variant": "c", + "limits.h": "c", + "time.h": "c", + "_stdtypes.h": "c", + "__memory": "c", + "__tree": "cpp", + "map": "cpp", + "codecvt": "cpp", + "cuchar": "cpp", + "uchar.h": "c", + "type_traits": "c", + "twr-ex.h": "c", + "twr-audio.h": "c" + }, //"C_Cpp.default.compilerPath": "C:/msys64/ucrt64/bin/gcc.exe", //"C_Cpp.default.compilerPath": "C:/msys64/ucrt64/bin/clang", "C_Cpp.default.compilerPath": "", // prevents default compiler include paths @@ -114,10 +116,12 @@ ], "C_Cpp.files.exclude": { }, + "C_Cpp.default.includePath": ["source/twr-bigint", "examples/twr-cpp"], "files.exclude": { }, "editor.tabSize": 3, "editor.detectIndentation": false, + "editor.insertSpaces": true, "files.eol": "\n", }, "tasks": {