xbintsc 0.5.37 → 0.5.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/src/cli/help.d.ts +1 -1
  2. package/dist/src/codegen/generator/state.js +1 -0
  3. package/dist/src/codegen/generator/state.js.map +1 -1
  4. package/dist/src/codegen/generator/tables.js +1 -0
  5. package/dist/src/codegen/generator/tables.js.map +1 -1
  6. package/dist/src/driver/bundler/deferred.d.ts +63 -0
  7. package/dist/src/driver/bundler/deferred.js +279 -0
  8. package/dist/src/driver/bundler/deferred.js.map +1 -0
  9. package/dist/src/driver/bundler/merge.js +214 -2
  10. package/dist/src/driver/bundler/merge.js.map +1 -1
  11. package/dist/src/driver/runtime-objects.d.ts +1 -1
  12. package/dist/src/driver/runtime-objects.js +1 -0
  13. package/dist/src/driver/runtime-objects.js.map +1 -1
  14. package/dist/src/driver/version.d.ts +1 -1
  15. package/dist/src/driver/version.js +1 -1
  16. package/dist/src/extensions/gui/html.js +89 -13
  17. package/dist/src/extensions/gui/html.js.map +1 -1
  18. package/dist/src/extensions/gui/index.js +1 -0
  19. package/dist/src/extensions/gui/index.js.map +1 -1
  20. package/dist/src/extensions/gui/three.d.ts +4 -3
  21. package/dist/src/extensions/gui/three.js +4 -3
  22. package/dist/src/extensions/gui/three.js.map +1 -1
  23. package/dist/tests/e2e/gui-script-import-order.test.d.ts +10 -0
  24. package/dist/tests/e2e/gui-script-import-order.test.js +161 -0
  25. package/dist/tests/e2e/gui-script-import-order.test.js.map +1 -0
  26. package/dist/tests/e2e/gui-webgl2-shadow.test.d.ts +10 -0
  27. package/dist/tests/e2e/gui-webgl2-shadow.test.js +108 -0
  28. package/dist/tests/e2e/gui-webgl2-shadow.test.js.map +1 -0
  29. package/dist/tests/e2e/url.test.d.ts +6 -0
  30. package/dist/tests/e2e/url.test.js +114 -0
  31. package/dist/tests/e2e/url.test.js.map +1 -0
  32. package/dist/tests/extensions/gui.test.js +22 -10
  33. package/dist/tests/extensions/gui.test.js.map +1 -1
  34. package/doc/gui/webgl.md +20 -10
  35. package/doc/gui-scripts.md +19 -19
  36. package/doc/gui.md +10 -8
  37. package/doc/implemented/fetch.md +5 -0
  38. package/doc/implemented/modules.md +13 -0
  39. package/doc/implemented/stdlib.md +17 -0
  40. package/doc/unimplemented/deviations.md +1 -0
  41. package/doc/zh-CN/gui/webgl.md +3 -3
  42. package/doc/zh-CN/gui-scripts.md +14 -13
  43. package/doc/zh-CN/gui.md +7 -6
  44. package/doc/zh-CN/implemented/fetch.md +4 -0
  45. package/doc/zh-CN/implemented/modules.md +10 -0
  46. package/doc/zh-CN/implemented/stdlib.md +15 -0
  47. package/doc/zh-CN/unimplemented/deviations.md +1 -0
  48. package/package.json +7 -7
  49. package/runtime/ext_gui/dom_api_webgl.cpp +10 -0
  50. package/runtime/ext_gui/dom_api_webgl2.cpp +33 -1
  51. package/runtime/ext_gui/gui_engine.h +2 -0
  52. package/runtime/ext_gui/script.cpp +31 -0
  53. package/runtime/ext_gui/webgl_glsl.h +3 -0
  54. package/runtime/ext_gui/webgl_glsl_unroll.cpp +283 -0
  55. package/runtime/ext_gui/webgl_msl.cpp +4 -0
  56. package/runtime/ext_gui/webgl_shader.cpp +12 -6
  57. package/runtime/rt.h +3 -0
  58. package/runtime/xt_fetch/async.inc +60 -0
  59. package/runtime/xt_url_object.c +463 -0
  60. package/src/codegen/generator/state.ts +1 -0
  61. package/src/codegen/generator/tables.ts +1 -0
  62. package/src/driver/bundler/deferred.ts +351 -0
  63. package/src/driver/bundler/merge.ts +219 -2
  64. package/src/driver/runtime-objects.ts +1 -0
  65. package/src/driver/version.ts +1 -1
  66. package/src/extensions/gui/html.ts +91 -15
  67. package/src/extensions/gui/index.ts +1 -0
  68. package/src/extensions/gui/three.ts +4 -3
@@ -0,0 +1,283 @@
1
+ /*
2
+ * xbintsc GUI engine — `#pragma unroll_loop_start` expansion.
3
+ *
4
+ * three.js marks its material light loops with
5
+ *
6
+ * #pragma unroll_loop_start
7
+ * for ( int i = 0; i < N; i ++ ) { ... UNROLLED_LOOP_INDEX ... [ i ] ... }
8
+ * #pragma unroll_loop_end
9
+ *
10
+ * and expands them in JavaScript before upload. On this runtime the expansion
11
+ * is best done here: the loop body indexes sampler arrays (`shadowMap[ i ]`),
12
+ * and Vulkan GLSL only accepts a *constant* sampler-array index. Expanding the
13
+ * pragma turns every `i` (and `UNROLLED_LOOP_INDEX`) into a literal, so the
14
+ * shadow-map chunks compile without relying on the regex engine or on three.js
15
+ * having unrolled them first. WebGL ignores the unknown pragmas either way.
16
+ *
17
+ * The transformation is deliberately small: only the `for (int i = A; i < B;
18
+ * i ++)` shape three.js emits is recognised. Anything else is left untouched.
19
+ */
20
+
21
+ #include "webgl_glsl.h"
22
+
23
+ #include <cctype>
24
+ #include <cstdlib>
25
+ #include <string>
26
+ #include <vector>
27
+
28
+ namespace xtgui {
29
+ namespace {
30
+
31
+ const char *const kStartPragma = "#pragma unroll_loop_start";
32
+ const char *const kEndPragma = "#pragma unroll_loop_end";
33
+
34
+ /** Skip whitespace and comments starting at `at`. */
35
+ size_t skip_space(const std::string &text, size_t at) {
36
+ while (at < text.size()) {
37
+ const char c = text[at];
38
+ if (std::isspace((unsigned char)c) != 0) {
39
+ at++;
40
+ continue;
41
+ }
42
+ if (c == '/' && at + 1 < text.size() && text[at + 1] == '/') {
43
+ at += 2;
44
+ while (at < text.size() && text[at] != '\n') at++;
45
+ continue;
46
+ }
47
+ if (c == '/' && at + 1 < text.size() && text[at + 1] == '*') {
48
+ at += 2;
49
+ while (at + 1 < text.size() && !(text[at] == '*' && text[at + 1] == '/')) at++;
50
+ if (at + 1 < text.size()) at += 2;
51
+ continue;
52
+ }
53
+ break;
54
+ }
55
+ return at;
56
+ }
57
+
58
+ /** Index just past the end of a `//` / `#` line starting at `at`. */
59
+ size_t line_end(const std::string &text, size_t at) {
60
+ while (at < text.size() && text[at] != '\n') at++;
61
+ return at;
62
+ }
63
+
64
+ /** Match the `}` for the `{` at `open`, skipping comments. */
65
+ size_t matching_brace(const std::string &text, size_t open, size_t *out) {
66
+ int depth = 0;
67
+ for (size_t i = open; i < text.size(); i++) {
68
+ const char c = text[i];
69
+ if (c == '/' && i + 1 < text.size() && text[i + 1] == '/') {
70
+ i += 2;
71
+ while (i < text.size() && text[i] != '\n') i++;
72
+ if (i < text.size()) continue;
73
+ break;
74
+ }
75
+ if (c == '/' && i + 1 < text.size() && text[i + 1] == '*') {
76
+ i += 2;
77
+ while (i + 1 < text.size() && !(text[i] == '*' && text[i + 1] == '/')) i++;
78
+ if (i + 1 < text.size()) i++;
79
+ continue;
80
+ }
81
+ if (c == '{') depth++;
82
+ if (c == '}') {
83
+ depth--;
84
+ if (depth == 0) {
85
+ *out = i;
86
+ return i;
87
+ }
88
+ }
89
+ }
90
+ return std::string::npos;
91
+ }
92
+
93
+ struct LoopHeader {
94
+ std::string variable;
95
+ long start = 0;
96
+ long end = 0;
97
+ bool inclusive = false;
98
+ };
99
+
100
+ /** Tokenise a for-header into identifiers, numbers and punctuation. */
101
+ std::vector<std::string> tokens(const std::string &text) {
102
+ std::vector<std::string> out;
103
+ size_t i = 0;
104
+ while (i < text.size()) {
105
+ const char c = text[i];
106
+ if (std::isspace((unsigned char)c) != 0) {
107
+ i++;
108
+ continue;
109
+ }
110
+ if (std::isalnum((unsigned char)c) != 0 || c == '_') {
111
+ size_t end = i + 1;
112
+ while (end < text.size() &&
113
+ (std::isalnum((unsigned char)text[end]) != 0 || text[end] == '_')) {
114
+ end++;
115
+ }
116
+ out.push_back(text.substr(i, end - i));
117
+ i = end;
118
+ continue;
119
+ }
120
+ if ((c == '+' && i + 1 < text.size() && text[i + 1] == '+') ||
121
+ (c == '-' && i + 1 < text.size() && text[i + 1] == '-')) {
122
+ out.push_back(text.substr(i, 2));
123
+ i += 2;
124
+ continue;
125
+ }
126
+ if ((c == '+' || c == '-' || c == '<' || c == '=' || c == '!') && i + 1 < text.size() &&
127
+ text[i + 1] == '=') {
128
+ out.push_back(text.substr(i, 2));
129
+ i += 2;
130
+ continue;
131
+ }
132
+ out.push_back(std::string(1, c));
133
+ i++;
134
+ }
135
+ return out;
136
+ }
137
+
138
+ bool parse_header(const std::string &header, LoopHeader *out) {
139
+ std::vector<std::string> parts = tokens(header);
140
+ size_t at = 0;
141
+ /* Optional qualifiers (`int`, `uint`, `highp int`, …). */
142
+ while (at < parts.size() && (parts[at] == "int" || parts[at] == "uint" || parts[at] == "highp" ||
143
+ parts[at] == "mediump" || parts[at] == "lowp")) {
144
+ at++;
145
+ }
146
+ if (at >= parts.size()) return false;
147
+ out->variable = parts[at++];
148
+ if (at >= parts.size() || parts[at] != "=") return false;
149
+ at++;
150
+ if (at >= parts.size()) return false;
151
+ char *end = nullptr;
152
+ out->start = std::strtol(parts[at].c_str(), &end, 10);
153
+ if (end == nullptr || *end != 0) return false;
154
+ at++;
155
+ if (at >= parts.size() || parts[at] != ";") return false;
156
+ at++;
157
+ /* `i < N` / `i <= N`; the repeated variable may be spelled differently. */
158
+ if (at >= parts.size()) return false;
159
+ at++;
160
+ if (at >= parts.size()) return false;
161
+ out->inclusive = parts[at] == "<=";
162
+ if (!out->inclusive && parts[at] != "<") return false;
163
+ at++;
164
+ if (at >= parts.size()) return false;
165
+ out->end = std::strtol(parts[at].c_str(), &end, 10);
166
+ if (end == nullptr || *end != 0) return false;
167
+ at++;
168
+ if (at >= parts.size() || parts[at] != ";") return false;
169
+ at++;
170
+ if (at >= parts.size()) return false;
171
+ at++;
172
+ if (at >= parts.size() || parts[at] != "++") return false;
173
+ return true;
174
+ }
175
+
176
+ /** Replace whole-word `name` with `value`. */
177
+ std::string substitute(const std::string &body, const std::string &name, long value) {
178
+ const std::string text = std::to_string(value);
179
+ std::string out;
180
+ out.reserve(body.size());
181
+ size_t i = 0;
182
+ while (i < body.size()) {
183
+ const char c = body[i];
184
+ if (std::isalpha((unsigned char)c) == 0 && c != '_') {
185
+ out += c;
186
+ i++;
187
+ continue;
188
+ }
189
+ size_t end = i + 1;
190
+ while (end < body.size() &&
191
+ (std::isalnum((unsigned char)body[end]) != 0 || body[end] == '_')) {
192
+ end++;
193
+ }
194
+ const std::string word = body.substr(i, end - i);
195
+ if (word == name || word == "UNROLLED_LOOP_INDEX") {
196
+ out += text;
197
+ } else {
198
+ out += word;
199
+ }
200
+ i = end;
201
+ }
202
+ return out;
203
+ }
204
+
205
+ } // namespace
206
+
207
+ std::string unroll_glsl_pragmas(const std::string &source) {
208
+ if (source.find(kStartPragma) == std::string::npos) return source;
209
+ std::string result;
210
+ size_t cursor = 0;
211
+ while (cursor < source.size()) {
212
+ const size_t pragma = source.find(kStartPragma, cursor);
213
+ if (pragma == std::string::npos) {
214
+ result.append(source, cursor, std::string::npos);
215
+ break;
216
+ }
217
+ result.append(source, cursor, pragma - cursor);
218
+ size_t at = line_end(source, pragma + std::string(kStartPragma).size());
219
+ const size_t after_header = skip_space(source, at);
220
+ if (source.compare(after_header, 3, "for") != 0) {
221
+ /* Not the shape three.js emits: keep the pragma and move on. */
222
+ result.append(source, pragma, at - pragma);
223
+ cursor = at;
224
+ continue;
225
+ }
226
+ size_t open_paren = skip_space(source, after_header + 3);
227
+ if (open_paren >= source.size() || source[open_paren] != '(') {
228
+ result.append(source, pragma, at - pragma);
229
+ cursor = at;
230
+ continue;
231
+ }
232
+ int depth = 0;
233
+ size_t close_paren = open_paren;
234
+ for (; close_paren < source.size(); close_paren++) {
235
+ if (source[close_paren] == '(') depth++;
236
+ if (source[close_paren] == ')') {
237
+ depth--;
238
+ if (depth == 0) break;
239
+ }
240
+ }
241
+ if (close_paren >= source.size()) {
242
+ result.append(source, pragma, at - pragma);
243
+ cursor = at;
244
+ continue;
245
+ }
246
+ LoopHeader loop;
247
+ if (!parse_header(source.substr(open_paren + 1, close_paren - open_paren - 1), &loop) ||
248
+ loop.end < loop.start || loop.end - loop.start > 4096) {
249
+ result.append(source, pragma, at - pragma);
250
+ cursor = at;
251
+ continue;
252
+ }
253
+ size_t open_brace = skip_space(source, close_paren + 1);
254
+ if (open_brace >= source.size() || source[open_brace] != '{') {
255
+ result.append(source, pragma, at - pragma);
256
+ cursor = at;
257
+ continue;
258
+ }
259
+ size_t close_brace = 0;
260
+ if (matching_brace(source, open_brace, &close_brace) == std::string::npos) {
261
+ result.append(source, pragma, at - pragma);
262
+ cursor = at;
263
+ continue;
264
+ }
265
+ size_t end = skip_space(source, close_brace + 1);
266
+ if (source.compare(end, std::string(kEndPragma).size(), kEndPragma) != 0) {
267
+ result.append(source, pragma, at - pragma);
268
+ cursor = at;
269
+ continue;
270
+ }
271
+ end = line_end(source, end + std::string(kEndPragma).size());
272
+ const std::string body = source.substr(open_brace + 1, close_brace - open_brace - 1);
273
+ const long last = loop.inclusive ? loop.end : loop.end - 1;
274
+ for (long i = loop.start; i <= last; i++) {
275
+ result += substitute(body, loop.variable, i);
276
+ result += '\n';
277
+ }
278
+ cursor = end;
279
+ }
280
+ return result;
281
+ }
282
+
283
+ } // namespace xtgui
@@ -36,6 +36,10 @@ bool cross_compile_msl(const std::vector<uint32_t> &spirv, bool fragment,
36
36
  * 0..num_samplers-1, so any other allocation would sample the wrong
37
37
  * texture (or none at all). */
38
38
  options.enable_decoration_binding = true;
39
+ /* Sampler arrays (three.js shadow maps) need MSL 2.0: with the default
40
+ * 1.2 target SPIRV-Cross rejects "MSL 2.0 or greater is required for
41
+ * arrays of textures". Every Metal device SDL_GPU runs on supports 2.0. */
42
+ options.msl_version = spirv_cross::CompilerMSL::Options::make_msl_version(2, 0);
39
43
  compiler.set_msl_options(options);
40
44
  *source = compiler.compile();
41
45
  *entry = compiler.get_cleansed_entry_point_name(
@@ -174,8 +174,12 @@ bool WebGLContext::compile_shader(uint32_t id) {
174
174
  WebGLShader *record = shader(id);
175
175
  if (record == nullptr) return false;
176
176
  const bool fragment = record->type == GL_FRAGMENT_SHADER;
177
+ /* Expand three.js's pragma-marked light loops first: a sampler-array index
178
+ * must be constant in Vulkan GLSL, and the expansion is the only place that
179
+ * knows the loop bounds. Scanning and rewriting then share one text. */
180
+ const std::string prepared = unroll_glsl_pragmas(record->source);
177
181
  GlslShaderInfo info;
178
- scan_glsl(record->source, fragment, &info);
182
+ scan_glsl(prepared, fragment, &info);
179
183
  if (!info.unsupported.empty()) {
180
184
  record->log = info.unsupported;
181
185
  record->compiled = false;
@@ -186,7 +190,7 @@ bool WebGLContext::compile_shader(uint32_t id) {
186
190
  assign_locations(info.varyings, {}, &layout.varyings);
187
191
  assign_locations(info.samplers, {}, &layout.samplers);
188
192
  layout.block_members = info.uniforms;
189
- const std::string source = rewrite_glsl(record->source, fragment, layout, info);
193
+ const std::string source = rewrite_glsl(prepared, fragment, layout, info);
190
194
  std::vector<uint32_t> spirv;
191
195
  record->compiled = compile_glsl(source, fragment, &spirv, &record->log);
192
196
  return record->compiled;
@@ -289,10 +293,12 @@ bool WebGLContext::link_program(uint32_t id) {
289
293
  return false;
290
294
  }
291
295
 
296
+ const std::string prepared_vertex = unroll_glsl_pragmas(vertex->source);
297
+ const std::string prepared_fragment = unroll_glsl_pragmas(fragment->source);
292
298
  GlslShaderInfo vs_info;
293
299
  GlslShaderInfo fs_info;
294
- scan_glsl(vertex->source, false, &vs_info);
295
- scan_glsl(fragment->source, true, &fs_info);
300
+ scan_glsl(prepared_vertex, false, &vs_info);
301
+ scan_glsl(prepared_fragment, true, &fs_info);
296
302
  if (!vs_info.unsupported.empty() || !fs_info.unsupported.empty()) {
297
303
  record->log = vs_info.unsupported.empty() ? fs_info.unsupported : vs_info.unsupported;
298
304
  record->linked = false;
@@ -361,8 +367,8 @@ bool WebGLContext::link_program(uint32_t id) {
361
367
  }
362
368
  fs_layout.varyings = vs_layout.varyings;
363
369
 
364
- const std::string vs_source = rewrite_glsl(vertex->source, false, vs_layout, vs_info);
365
- const std::string fs_source = rewrite_glsl(fragment->source, true, fs_layout, fs_info);
370
+ const std::string vs_source = rewrite_glsl(prepared_vertex, false, vs_layout, vs_info);
371
+ const std::string fs_source = rewrite_glsl(prepared_fragment, true, fs_layout, fs_info);
366
372
  std::vector<uint32_t> vs_spirv;
367
373
  std::vector<uint32_t> fs_spirv;
368
374
  std::string log;
package/runtime/rt.h CHANGED
@@ -305,6 +305,9 @@ xt_value xt_date_static(xt_value name, int32_t argc, xt_value *argv);
305
305
  xt_value xt_regexp_ctor(int32_t argc, xt_value *argv);
306
306
 
307
307
  /* -- node:url ------------------------------------------------------------- */
308
+ xt_value xt_url_ctor(int32_t argc, xt_value *argv);
309
+ /** Resolve a `fetch` reference to a filesystem path (file: URLs and relative paths). */
310
+ int xt_url_file_path(const char *url, char *out, size_t size);
308
311
  xt_value xt_url_path_to_file_url(int32_t argc, xt_value *argv);
309
312
  xt_value xt_url_file_url_to_path(int32_t argc, xt_value *argv);
310
313
 
@@ -201,6 +201,66 @@ static void xt_fetch_hop_complete(xt_async_job *job) {
201
201
 
202
202
  /* Main thread: validate the current URL and queue one exchange. */
203
203
  static void xt_fetch_start(xt_fetch_state *state) {
204
+ /* `file:` URLs and references with no scheme (a relative path) read a local
205
+ * file. This is what lets a compiled page load an asset from disk without an
206
+ * HTTP server; the response is built on this thread because a local read is
207
+ * already complete. */
208
+ char path[4096];
209
+ if (xt_url_file_path(state->url, path, sizeof(path))) {
210
+ if (strcmp(state->method, "GET") != 0 && strcmp(state->method, "HEAD") != 0) {
211
+ xt_fetch_state_reject(state, "fetch failed: file URLs only support GET or HEAD");
212
+ return;
213
+ }
214
+ FILE *file = fopen(path, "rb");
215
+ if (!file) {
216
+ xt_fetch_state_reject(state, "fetch failed: cannot read file");
217
+ return;
218
+ }
219
+ size_t capacity = 0;
220
+ size_t length = 0;
221
+ char *body = NULL;
222
+ char chunk[16384];
223
+ for (;;) {
224
+ size_t count = fread(chunk, 1, sizeof(chunk), file);
225
+ if (count > 0) {
226
+ if (length + count + 1 > capacity) {
227
+ capacity = (length + count + 1) * 2;
228
+ char *grown = (char *)realloc(body, capacity);
229
+ if (!grown) {
230
+ free(body);
231
+ fclose(file);
232
+ xt_fetch_state_reject(state, "fetch failed");
233
+ return;
234
+ }
235
+ body = grown;
236
+ }
237
+ memcpy(body + length, chunk, count);
238
+ length += count;
239
+ }
240
+ if (count < sizeof(chunk)) break;
241
+ }
242
+ fclose(file);
243
+
244
+ char href[4300];
245
+ if (path[0] == '/') {
246
+ snprintf(href, sizeof(href), "file://%s", path);
247
+ } else {
248
+ snprintf(href, sizeof(href), "file:///%s", path);
249
+ }
250
+ xt_value headers = xt_headers_new();
251
+ char lengthText[32];
252
+ snprintf(lengthText, sizeof(lengthText), "%zu", length);
253
+ xt_headers_set_raw(headers, "content-length", lengthText);
254
+ xt_headers_set_raw(headers, "content-type", "application/octet-stream");
255
+ size_t bodyLength = strcmp(state->method, "HEAD") == 0 ? 0 : length;
256
+ xt_value response = xt_response_new(headers, 200, "OK", href, body, bodyLength);
257
+ free(body);
258
+ xt_value promise = state->promise;
259
+ xt_fetch_state_free(state);
260
+ xt_promise_fulfill(promise, response);
261
+ return;
262
+ }
263
+
204
264
  xt_fetch_url parsed;
205
265
  if (xt_fetch_parse_url(state->url, &parsed) != 0) {
206
266
  xt_fetch_state_reject(state, "Failed to parse URL");