Skip to content

Emscripten compilation and execution in browsers - #787

Draft
j8asic wants to merge 34 commits into
shader-slang:mainfrom
j8asic:main
Draft

Emscripten compilation and execution in browsers#787
j8asic wants to merge 34 commits into
shader-slang:mainfrom
j8asic:main

Conversation

@j8asic

@j8asic j8asic commented Feb 6, 2026

Copy link
Copy Markdown
image

This is a draft PR where SGL compiles using Emscripten.

It relies on RHI PR to be merged: shader-slang/slang-rhi#609 and Slang's PR shader-slang/slang#9818 to produce WASM libraries to be automatically used for RHI/SGL compilation

Summary by CodeRabbit

  • New Features

    • WebAssembly (Emscripten) platform support added for browser deployment
    • New browser-hosted demos: render pipeline and simple compute with HTML UIs
  • Build System

    • CMake presets and targets for Emscripten/WebAssembly builds
    • Build logic adapts features for Web builds (e.g., optional native acceleration disabled on WASM)
  • Chores

    • Packaging and post-build steps added to publish WASM assets alongside builds

@j8asic
j8asic requested a review from a team as a code owner February 6, 2026 14:47
@CLAassistant

CLAassistant commented Feb 6, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@j8asic
j8asic marked this pull request as draft February 6, 2026 18:42

@ccummingsNV ccummingsNV left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exciting stuff and looks mostly sane to me, but I'd need @skallweitNV to take a look as he understands the build process better than me.

I don't think we can really ship this without it being part of the testing suite either. Currently almost all tests run on all platforms. How do we want to approach that.

@skallweitNV

Copy link
Copy Markdown
Contributor

@j8asic thanks for the contribution. This is really cool to see! Let's first merge the slang-rhi PR and then get to this one after that.

@coderabbitai

coderabbitai Bot commented Feb 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: daf01f73-8ebe-4d05-9266-65fb3651442b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds comprehensive Emscripten/WebAssembly support: platform detection macros, build presets and toolchain, many conditional compile paths and stubs for unsupported APIs, WASM example targets and HTML hosts, and runtime changes to use a window/surface swapchain for browser rendering.

Changes

Cohort / File(s) Summary
Build configs & presets
\.gitignore, CMakeLists.txt, CMakePresets.json
Add Emscripten preset and build dir (build.em), detect SGL_EMSCRIPTEN in CMake, adjust CUDA discovery (SGL_HAS_CUDA), and ignore build.em/.
External deps & third-party
external/CMakeLists.txt
Conditional handling for Emscripten: skip/add different Slang/FetchContent flow, disable glfw/nanothread where appropriate, add SGL_LOCAL_SLANG_RHI* cache options and WGPU/Dawn toggles, and adjust imported Slang linkage for WASM.
Core macros & platform selection
src/sgl/core/macros.h
Introduce SGL_EMSCRIPTEN, SGL_COMPILER_EMSCRIPTEN, SGL_ARCH_WASM, SGL_PLATFORM_EMSCRIPTEN, and fix boolean operator bug.
Library build & linking
src/sgl/CMakeLists.txt
Build sgl STATIC for Emscripten, add platform_emscripten.cpp, suppress linking of some deps on WASM, and add Emscripten-specific link flags.
Platform layer & windowing
src/sgl/core/platform.h, src/sgl/core/platform_emscripten.cpp, src/sgl/core/window.cpp, src/sgl/device/surface.cpp
New Emscripten platform implementation (many stubbed APIs), add canvasSelector to WindowHandle, use WGPU canvas handle for surface creation, gate GLFW native includes/gamepad on WASM.
Threading, fs, and mmap stubs
src/sgl/core/thread.h, src/sgl/core/file_system_watcher.*, src/sgl/core/memory_mapped_file.*, src/sgl/core/lmdb_cache.cpp, src/sgl/core/file_stream.cpp, src/sgl/core/object.h, src/sgl/core/string.cpp
Replace nanothread with single-threaded mocks for Emscripten; disable file-watcher threads/mutexes on WASM; provide mmap/page-size and LMDB stub implementations and small platform-specific adjustments.
CUDA gating & device changes
CMakeLists.txt, src/sgl/device/*.{cpp,h}, src/sgl/device/native_handle_traits.h
Introduce SGL_HAS_CUDA logic (disabled for Emscripten/macOS), guard CUDA translation units, APIs, members, and native-handle traits behind SGL_HAS_CUDA.
Shader, examples & WASM targets
examples/..., examples/wasm/*, examples/render_pipeline/*.{cpp,slang}
Add wasm example CMake, new HTML pages and JS wasm module hooks, set SGL_EXAMPLE_DIR="." for wasm targets, refactor render_pipeline to windowed swapchain loop, and add WGPU-compatible shader path.
Utilities & debug
src/sgl/utils/renderdoc.{h,cpp}
RenderDoc APIs stubbed out for Emscripten (inline false-returning functions); full implementation preserved for other platforms.
Misc. small changes
examples/simple_compute/simple_compute.cpp, examples/CMakeLists.txt
Adjust example include path under __EMSCRIPTEN__; only add wasm examples when SGL_EMSCRIPTEN.

Sequence Diagram

sequenceDiagram
    participant App as Example App
    participant Window as Browser Window / Canvas
    participant Surface as Surface/Swapchain
    participant Device as SGL Device
    participant RHI as RHI Backend (WGPU)
    participant GPU as GPU/WASM Runtime

    App->>Window: create_window(canvasSelector)
    Window-->>App: WindowHandle (canvasSelector)
    App->>Surface: create_surface(WindowHandle)
    Surface->>RHI: configure(preferred_format)
    RHI-->>Surface: configured

    loop Main loop
        App->>Window: poll_events()
        Window-->>App: events

        alt surface configured
            App->>Surface: acquire_next_image()
            Surface->>RHI: get_presentable_image
            RHI-->>Surface: image_view

            App->>Device: create_command_buffer()
            Device-->>App: command_buffer

            App->>Device: record_render_commands(image_view)
            Device->>RHI: submit(commands)
            RHI->>GPU: execute

            App->>Surface: present()
            Surface->>RHI: present image
        end

        App->>App: emscripten_sleep(0)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • skallweitNV
  • ccummingsNV
  • bmillsNV

Poem

🐰
I nibble bytes by moonlit tide,
Into WebAssembly I hop and glide.
Canvas gleams, the shaders hum,
No threads to chase — the loop goes numb.
SGL in browser — carrots for the sun!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and accurately describes the main objective of the changeset: enabling Emscripten compilation and browser execution for WebAssembly.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@j8asic

j8asic commented Feb 24, 2026

Copy link
Copy Markdown
Author

thanks @ccummingsNV @skallweitNV; bumping to the latest slang version and merging the slang-rhi PR, this is almost ready for review.
one final question from my side: what does thread.h do, or better to say, what do you want it to do in the context of browsers?

@j8asic
j8asic marked this pull request as ready for review April 22, 2026 19:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/sgl/device/cuda_utils.cpp (1)

3-408: ⚠️ Potential issue | 🔴 Critical

Use a numeric feature guard #if SGL_HAS_CUDA instead of #ifdef.

The macro SGL_HAS_CUDA is defined as a numeric value (0 or 1) via CMake. The current code at line 3 uses #ifdef SGL_HAS_CUDA before including any config header, which is inconsistent with the rest of the codebase. All other files guard CUDA code with #if SGL_HAS_CUDA after including the appropriate config headers. For numeric feature macros, #if is the correct approach.

Include "sgl/core/config.h" first, then replace #ifdef with #if:

Proposed fix
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
-#ifdef SGL_HAS_CUDA
+#include "sgl/core/config.h"
+
+#if SGL_HAS_CUDA
 
 `#include` "cuda_utils.h"
 } // namespace sgl::cuda
 
-#endif
+#endif // SGL_HAS_CUDA
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/device/cuda_utils.cpp` around lines 3 - 408, At the top of this CUDA
implementation (sgl::cuda namespace / functions like get_current_device_index
and class Device), include the project config header by adding `#include`
"sgl/core/config.h" before the feature guard, and replace the existing
preprocessor check from "#ifdef SGL_HAS_CUDA" to "#if SGL_HAS_CUDA" (keeping the
trailing `#endif` unchanged) so the numeric CMake-defined macro is tested
correctly.
src/sgl/core/memory_mapped_file.cpp (1)

201-241: ⚠️ Potential issue | 🟠 Major

Move the Emscripten remap branch out of the Linux/macOS block.

The #elif SGL_EMSCRIPTEN branch at line 221 is nested inside #elif SGL_LINUX || SGL_MACOS, making it unreachable. When SGL_EMSCRIPTEN is defined, the outer condition evaluates to false (Emscripten is neither Linux nor macOS), so the entire block is skipped. The function then returns true without setting m_mapped_data or m_mapped_size. Move the Emscripten handling to a top-level #elif SGL_EMSCRIPTEN branch before the Linux/macOS block and return false.

Proposed fix
 `#if` SGL_WINDOWS
     DWORD offsetLow = DWORD(offset & 0xFFFFFFFF);
     DWORD offsetHigh = DWORD(offset >> 32);
 
     // Create new mapping.
     m_mapped_data = ::MapViewOfFile(m_mapped_file, FILE_MAP_READ, offsetHigh, offsetLow, mapped_size);
     if (!m_mapped_data)
         m_mapped_size = 0;
     m_mapped_size = mapped_size;
+#elif SGL_EMSCRIPTEN
+    // mmap is not available in the browser build.
+    m_mapped_data = nullptr;
+    m_mapped_size = 0;
+    return false;
 `#elif` SGL_LINUX || SGL_MACOS
     // Create new mapping.
 `#if` SGL_LINUX
     m_mapped_data = ::mmap64(NULL, mapped_size, PROT_READ, MAP_SHARED, m_file, offset);
 `#elif` SGL_MACOS
     m_mapped_data = ::mmap(NULL, mapped_size, PROT_READ, MAP_SHARED, m_file, offset);
-#elif SGL_EMSCRIPTEN
-    m_mapped_data = MAP_FAILED;
 `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/core/memory_mapped_file.cpp` around lines 201 - 241, The Emscripten
branch is wrongly nested inside the Linux/macOS conditional so when
SGL_EMSCRIPTEN is defined m_mapped_data and m_mapped_size are never set; move
the SGL_EMSCRIPTEN handling out to its own top-level `#elif` SGL_EMSCRIPTEN branch
(parallel to the Windows and Linux/macOS branches) so it executes independently,
set m_mapped_data = MAP_FAILED (or nullptr equivalent), set/clear m_mapped_size
appropriately and return false on failure; update the branch locations around
the existing MapViewOfFile (Windows) and mmap/madvise (Linux/macOS) code and
ensure symbols m_mapped_data, m_mapped_size, SGL_EMSCRIPTEN, MAP_FAILED,
mmap/mmap64, and ::madvise are used consistently.
src/sgl/utils/renderdoc.cpp (1)

5-174: ⚠️ Potential issue | 🔴 Critical

Use #if !SGL_EMSCRIPTEN instead of #ifndef.

The macro SGL_EMSCRIPTEN is defined as (SGL_COMPILER == SGL_COMPILER_EMSCRIPTEN) in src/sgl/core/macros.h and evaluates to 0 or 1. Using #ifndef SGL_EMSCRIPTEN always evaluates to false (since the macro IS defined), making the entire RenderDoc implementation unreachable and preventing it from compiling on any build.

Proposed fix
-#ifndef SGL_EMSCRIPTEN
+#if !SGL_EMSCRIPTEN
-#endif // SGL_EMSCRIPTEN
+#endif // !SGL_EMSCRIPTEN

Note: The same incorrect #ifndef pattern is used in renderdoc.h (line 11), window.cpp (lines 28, 46, 571), and string.cpp (line 122). They all need the same fix for consistency with the correct pattern already used in lmdb_cache.cpp (line 8).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/utils/renderdoc.cpp` around lines 5 - 174, The file uses an `#ifndef`
SGL_EMSCRIPTEN guard which is wrong because SGL_EMSCRIPTEN is defined as 0/1;
change the top-level preprocessor guard to `#if` !SGL_EMSCRIPTEN (and similarly
update the matching endif comment if present) so the renderdoc::API class and
its functions (API, API::start_frame_capture, API::end_frame_capture,
is_available, start_frame_capture, end_frame_capture, is_frame_capturing) are
compiled when not targeting Emscripten; apply the same fix to the other affected
translation units that use the same pattern (renderdoc.h, window.cpp,
string.cpp) for consistency.
src/sgl/core/file_system_watcher.cpp (1)

77-148: ⚠️ Potential issue | 🟡 Minor

Remove unnecessary directory scan on Emscripten.

The state->files member is populated on Emscripten (line 146-148) but never consumed, since the polling thread does not compile there:

`#if` !SGL_LINUX
    state->files = get_directory_files(state->desc.directory);
`#endif`

On Emscripten, the struct member exists (guarded only by #if !SGL_LINUX), but the thread that reads it is guarded by #if !SGL_LINUX && !SGL_EMSCRIPTEN (line 277). This wastes work scanning the directory on a platform where the result is unused. Align the guard:

🔧 Suggested change
-#if !SGL_LINUX
+#if !SGL_LINUX && !SGL_EMSCRIPTEN
     state->files = get_directory_files(state->desc.directory);
 `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/core/file_system_watcher.cpp` around lines 77 - 148, The directory
scan in FileSystemWatcher::add_watch currently populates state->files via
get_directory_files even on Emscripten where the polling thread
(m_thread/thread_func) is not compiled; change the guard around that assignment
so it excludes Emscripten (i.e., only run state->files =
get_directory_files(...) when not SGL_LINUX and not SGL_EMSCRIPTEN) to avoid
doing unnecessary work on Emscripten platforms, keeping all other logic (id
generation, state initialization, and Linux inotify handling) unchanged.
🧹 Nitpick comments (14)
examples/simple_compute/simple_compute.cpp (1)

15-19: Use the project platform macro and snake_case local constant.

This works, but while adding the Emscripten branch, consider using SGL_EMSCRIPTEN consistently and renaming the local constant to example_dir. As per coding guidelines, C++ functions and variables use snake_case.

♻️ Optional cleanup
-#ifdef __EMSCRIPTEN__
-static const std::filesystem::path EXAMPLE_DIR(".");
+#if SGL_EMSCRIPTEN
+static const std::filesystem::path example_dir(".");
 `#else`
-static const std::filesystem::path EXAMPLE_DIR(SGL_EXAMPLE_DIR);
+static const std::filesystem::path example_dir(SGL_EXAMPLE_DIR);
 `#endif`
 ...
-            .compiler_options = {.include_paths = {EXAMPLE_DIR}},
+            .compiler_options = {.include_paths = {example_dir}},

Also applies to: 30-30

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/simple_compute/simple_compute.cpp` around lines 15 - 19, Rename the
platform-conditional local constant EXAMPLE_DIR to snake_case example_dir and
switch the preprocessor check to use the project macro SGL_EMSCRIPTEN;
specifically update the branch that currently tests __EMSCRIPTEN__ to use
SGL_EMSCRIPTEN and replace occurrences of EXAMPLE_DIR with example_dir while
preserving the same std::filesystem::path initialization from SGL_EXAMPLE_DIR or
"." in the emscripten case so all uses (e.g., in this file and the other
occurrence at lines ~30) follow the project's naming and macro conventions.
src/sgl/core/platform.h (1)

33-34: Rename field to snake_case and add Doxygen documentation.

canvasSelector must follow C++ naming conventions: use snake_case for variables and add Doxygen /// comments for public fields. Update the initializer in window.cpp and the consumer in surface.cpp.

Refactor
 `#elif` SGL_EMSCRIPTEN
-    const char* canvasSelector = nullptr;
+    /// CSS selector for the target WebGPU canvas.
+    const char* canvas_selector = nullptr;
 `#endif`

Update call sites:

  • src/sgl/core/window.cpp:421: handle.canvasSelectorhandle.canvas_selector
  • src/sgl/device/surface.cpp:27: window_handle.canvasSelectorwindow_handle.canvas_selector
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/core/platform.h` around lines 33 - 34, Rename the public field
canvasSelector to snake_case canvas_selector and add a Doxygen triple-slash
comment (///) describing the field in the struct/class guarded by
SGL_EMSCRIPTEN; update its initializer in the window initialization code that
sets handle.canvasSelector to use handle.canvas_selector, and update the
consumer in the surface code that reads window_handle.canvasSelector to use
window_handle.canvas_selector; ensure all references and includes compile after
the rename and that the Doxygen comment describes the purpose (e.g., selector
for the Emscripten canvas element).
examples/wasm/CMakeLists.txt (1)

1-41: Consider a helper macro to deduplicate the two WASM example targets.

Both targets repeat the same six operations (add_executable, cxx_std_20, link sgl, preload .slang, define SGL_EXAMPLE_DIR, post-build copy). A small helper keeps the list of examples easy to extend.

Also note: --preload-file already bundles the .slang into the Emscripten virtual filesystem, so the POST_BUILD copy next to the output duplicates the shader on disk — keep it if you need it for out-of-bundle access, otherwise it can be dropped.

♻️ Suggested refactor
-# Render Pipeline Example
-add_executable(render-pipeline-wasm
-    ../render_pipeline/render_pipeline.cpp
-)
-
-target_compile_features(render-pipeline-wasm PRIVATE cxx_std_20)
-target_link_libraries(render-pipeline-wasm PRIVATE sgl)
-target_link_options(render-pipeline-wasm PRIVATE
-    "SHELL:--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../render_pipeline/render_pipeline.slang@render_pipeline.slang"
-)
-target_compile_definitions(render-pipeline-wasm PRIVATE SGL_EXAMPLE_DIR=".")
-
-add_custom_command(TARGET render-pipeline-wasm POST_BUILD
-    COMMAND ${CMAKE_COMMAND} -E copy
-        ${CMAKE_CURRENT_SOURCE_DIR}/../render_pipeline/render_pipeline.slang
-        $<TARGET_FILE_DIR:render-pipeline-wasm>/render_pipeline.slang
-    COMMAND ${CMAKE_COMMAND} -E copy
-        ${CMAKE_CURRENT_SOURCE_DIR}/render_pipeline.html
-        $<TARGET_FILE_DIR:render-pipeline-wasm>/render_pipeline.html
-)
-
-# Simple Compute Example
-add_executable(simple-compute-wasm
-    ../simple_compute/simple_compute.cpp
-)
-
-target_compile_features(simple-compute-wasm PRIVATE cxx_std_20)
-target_link_libraries(simple-compute-wasm PRIVATE sgl)
-target_link_options(simple-compute-wasm PRIVATE
-    "SHELL:--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../simple_compute/simple_compute.slang@simple_compute.slang"
-)
-target_compile_definitions(simple-compute-wasm PRIVATE SGL_EXAMPLE_DIR=".")
-
-add_custom_command(TARGET simple-compute-wasm POST_BUILD
-    COMMAND ${CMAKE_COMMAND} -E copy
-        ${CMAKE_CURRENT_SOURCE_DIR}/../simple_compute/simple_compute.slang
-        $<TARGET_FILE_DIR:simple-compute-wasm>/simple_compute.slang
-    COMMAND ${CMAKE_COMMAND} -E copy
-        ${CMAKE_CURRENT_SOURCE_DIR}/simple_compute.html
-        $<TARGET_FILE_DIR:simple-compute-wasm>/simple_compute.html
-)
+macro(sgl_add_wasm_example target src_dir name)
+    add_executable(${target} ${CMAKE_CURRENT_SOURCE_DIR}/../${src_dir}/${name}.cpp)
+    target_compile_features(${target} PRIVATE cxx_std_20)
+    target_link_libraries(${target} PRIVATE sgl)
+    target_link_options(${target} PRIVATE
+        "SHELL:--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../${src_dir}/${name}.slang@${name}.slang"
+    )
+    target_compile_definitions(${target} PRIVATE SGL_EXAMPLE_DIR=".")
+    add_custom_command(TARGET ${target} POST_BUILD
+        COMMAND ${CMAKE_COMMAND} -E copy
+            ${CMAKE_CURRENT_SOURCE_DIR}/${name}.html
+            $<TARGET_FILE_DIR:${target}>/${name}.html
+    )
+endmacro()
+
+sgl_add_wasm_example(render-pipeline-wasm render_pipeline render_pipeline)
+sgl_add_wasm_example(simple-compute-wasm  simple_compute  simple_compute)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/wasm/CMakeLists.txt` around lines 1 - 41, Both WASM example targets
(render-pipeline-wasm and simple-compute-wasm) duplicate the same CMake steps;
add a helper macro/function (e.g., add_wasm_example(NAME SOURCE
SLANG_HTML_BASENAME)) that performs add_executable, target_compile_features(...
cxx_std_20), target_link_libraries(... sgl), target_link_options to add the
"SHELL:--preload-file
${CMAKE_CURRENT_SOURCE_DIR}/../<example>/<basename>.slang@<basename>.slang"
entry, target_compile_definitions(SGL_EXAMPLE_DIR="."), and the
add_custom_command POST_BUILD that copies the .slang and .html; update calls for
render-pipeline-wasm and simple-compute-wasm to use this macro and optionally
remove the POST_BUILD copy if you rely solely on --preload-file for the virtual
FS.
examples/render_pipeline/render_pipeline.cpp (2)

130-135: Destructor order with device->close().

~App() calls device->close() before the ref-counted members (pipeline, program, input_layout, buffers, surface) are destroyed. This works today because Device::close() (see device.cpp:460-498) only tears down device-owned helpers (blitter, printer, fences, slang_session, cuda_device) and leaves the rhi_device alive until ~Device. But it is a subtle ordering that will silently break if close() ever releases m_rhi_device eagerly. Consider explicitly resetting the dependent refs first, e.g.:

 ~App()
 {
-    if (device)
-        device->close();
+    pipeline.reset();
+    program.reset();
+    input_layout.reset();
+    index_buffer.reset();
+    vertex_buffer.reset();
+    surface.reset();
+    if (device)
+        device->close();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/render_pipeline/render_pipeline.cpp` around lines 130 - 135, The
destructor ~App() currently calls device->close() before releasing ref-counted
members (pipeline, program, input_layout, buffers, surface), which can break if
Device::close() later releases m_rhi_device; update ~App() to first explicitly
reset/release all dependent ref-counted members (call reset() or clear() on
pipeline, program, input_layout, surface and any buffers/containers holding RHI
refs) and only after those are cleared call device->close() (and then release
the device itself), so the dependent objects are destroyed before the device
teardown.

137-154: Prefer emscripten_set_main_loop (or at least a non-zero sleep) for the browser loop.

The current while (!should_close()) { main_loop(); emscripten_sleep(0); } pattern works under Asyncify, but:

  • emscripten_sleep(0) yields to the JS event loop but does not align with the browser's animation frame, and will effectively run at ~100% CPU.
  • Using emscripten_set_main_loop(..., 0, 1) (fps=0 → rAF) is the idiomatic browser integration, removes the need for Asyncify for the main loop itself, and integrates naturally with requestAnimationFrame.

At minimum, bump the sleep (e.g., emscripten_sleep(16)) to roughly match vsync.

♻️ Sketch using emscripten_set_main_loop_arg
-    {
-        App app;
-
-        while (!app.window->should_close()) {
-            app.main_loop();
-#ifdef __EMSCRIPTEN__
-            emscripten_sleep(0);
-#endif
-        }
-    }
+    {
+        App app;
+#ifdef __EMSCRIPTEN__
+        emscripten_set_main_loop_arg(
+            [](void* arg) { static_cast<App*>(arg)->main_loop(); },
+            &app, 0 /* fps -> rAF */, 1 /* simulate infinite loop */);
+#else
+        while (!app.window->should_close())
+            app.main_loop();
+#endif
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/render_pipeline/render_pipeline.cpp` around lines 137 - 154, The
current main loop in main() spins with emscripten_sleep(0); replace the busy
loop by using emscripten_set_main_loop_arg to run a callback that calls
app.main_loop() and checks app.window->should_close(), calling
emscripten_cancel_main_loop() to stop when needed; alternatively, if you must
keep the loop, change emscripten_sleep(0) to a non-zero value like
emscripten_sleep(16) to throttle CPU. Locate the main function and the App
instance (symbols: main, App, app, app.main_loop(), app.window->should_close())
and implement the main-loop callback (used with emscripten_set_main_loop_arg) or
the increased sleep to align with vsync.
CMakePresets.json (1)

201-213: Prefer CMAKE_EXE_LINKER_FLAGS_INIT and deduplicate with target_link_options.

Two small improvements to the new emscripten preset:

  1. CMAKE_CXX_FLAGS_INIT (already used here) is the _INIT form, so the user can later add flags. CMAKE_EXE_LINKER_FLAGS on the next line is the non-_INIT form, which is sticky in the cache and harder to override. For symmetry and override-friendliness, use CMAKE_EXE_LINKER_FLAGS_INIT.
  2. -sUSE_GLFW=3, -sALLOW_MEMORY_GROWTH, and -fwasm-exceptions are also set in src/sgl/CMakeLists.txt via target_link_options(sgl PUBLIC ...). Pick one source of truth — ideally the target-level options (PUBLIC propagation handles executables linked against sgl) — and drop the duplicates from the preset to prevent drift.
  3. Document -include cstdlib: it's a non-obvious workaround (presumably for a missing declaration in an Emscripten header) and a short comment via a JSON "description" field or a commit-log note will help future maintainers.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CMakePresets.json` around lines 201 - 213, Update the emscripten preset to
use CMAKE_EXE_LINKER_FLAGS_INIT (mirror CMAKE_CXX_FLAGS_INIT) so linker flags
remain override-friendly, remove duplicate flags that are already set at target
level (drop -sUSE_GLFW=3, -sALLOW_MEMORY_GROWTH, -fwasm-exceptions from the
preset) and rely on the existing target_link_options(sgl PUBLIC ...) in
src/sgl/CMakeLists.txt for those options, and add a short note documenting the
non-obvious "-include cstdlib" workaround (e.g., in the preset "description"
field or commit message) so its purpose is recorded for future maintainers.
src/sgl/device/device.cpp (1)

73-93: Standardize WASM detection on SGL_EMSCRIPTEN instead of SLANG_WASM.

This file uses SLANG_WASM (from external Slang headers) for downstream compiler skip and automatic device type selection, while the rest of the codebase standardizes on SGL_EMSCRIPTEN (defined in src/sgl/core/macros.h). The inconsistency creates maintenance risk—SLANG_WASM may not be reliably synchronized with SGL_EMSCRIPTEN, which is derived from __EMSCRIPTEN__. Replace both instances at lines 73 and 84 with SGL_EMSCRIPTEN to align with the codebase standard.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/device/device.cpp` around lines 73 - 93, Replace uses of SLANG_WASM
in src/sgl/device/device.cpp with the project-standard SGL_EMSCRIPTEN: change
the first preprocessor guard from `#if` !SLANG_WASM to `#if` !SGL_EMSCRIPTEN (around
the setDownstreamCompilerPath loop) and change the automatic device-type branch
from `#if` SLANG_WASM to `#if` SGL_EMSCRIPTEN (the block that sets m_desc.type =
DeviceType::wgpu). Ensure SGL_EMSCRIPTEN is available in this translation unit
(include the header that defines it, e.g., sgl/core/macros.h) if not already
included.
src/sgl/core/thread.h (2)

8-10: Use SGL_EMSCRIPTEN for consistency.

Since sgl/core/macros.h is already included above, prefer the project macro over the compiler-predefined one to match the rest of the file (e.g., line 21) and the convention adopted in this PR.

♻️ Proposed change
-#ifndef __EMSCRIPTEN__
+#ifndef SGL_EMSCRIPTEN
 `#include` <nanothread/nanothread.h>
-#endif
+#endif
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/core/thread.h` around lines 8 - 10, Replace the compiler macro check
__EMSCRIPTEN__ with the project macro SGL_EMSCRIPTEN in the conditional include
at the top of thread.h so it matches the rest of the file and the project's
convention; specifically change the `#ifndef/__EMSCRIPTEN__` guard surrounding the
nanothread include to use SGL_EMSCRIPTEN (the file already includes
sgl/core/macros.h above), keeping the include and surrounding preprocessor
structure identical otherwise.

21-130: Hoist blocked_range template outside #ifdef to eliminate duplication.

blocked_range<Int> is defined identically at line 40 (Emscripten) and line 214 (nanothread). Since it has no dependency on nanothread, move it before the #ifdef SGL_EMSCRIPTEN to reduce code duplication.

Regarding the missing nanothread symbols: task_query, task_time, and task_time_rel are not used anywhere in the codebase, so their absence from the Emscripten mock is not a practical concern. The do_async, TaskGroup, and other public APIs are properly mocked for Emscripten builds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/core/thread.h` around lines 21 - 130, The duplicated
blocked_range<Int> definition should be hoisted out of the SGL_EMSCRIPTEN
conditional: move the blocked_range template (including its iterator, blocks(),
begin()/end(), block_size() and private members) above the `#ifdef` SGL_EMSCRIPTEN
so both Emscripten and non-Emscripten paths reuse it; leave the Emscripten mocks
for do_async, parallel_for, parallel_for_async, TaskGroup, and task_* wrappers
as-is, and do not add unused nanothread symbols (task_query, task_time,
task_time_rel) to the Emscripten section since they aren’t referenced.
external/CMakeLists.txt (4)

132-139: Redundant inner if(NOT SGL_LOCAL_SLANG).

Line 132 is already the elseif(NOT SGL_EMSCRIPTEN) branch of if(SGL_LOCAL_SLANG), so SGL_LOCAL_SLANG is guaranteed to be false here — the inner conditional on line 133 is always true.

♻️ Proposed change
 elseif(NOT SGL_EMSCRIPTEN)
-    if(NOT SGL_LOCAL_SLANG)
-        sgl_download_package(slang ${SLANG_URL})
-    endif()
-
+    sgl_download_package(slang ${SLANG_URL})
     set(SLANG_DIR ${slang_SOURCE_DIR})
     set(SLANG_INCLUDE_DIR ${slang_SOURCE_DIR}/include)
 endif()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@external/CMakeLists.txt` around lines 132 - 139, The inner conditional if(NOT
SGL_LOCAL_SLANG) inside the elseif(NOT SGL_EMSCRIPTEN) branch is redundant
because SGL_LOCAL_SLANG is already false in this branch; remove that inner
if/endif and unindent its body so sgl_download_package(slang ${SLANG_URL}) and
the subsequent set(...) calls (SLANG_DIR, SLANG_INCLUDE_DIR) execute directly
within the elseif(NOT SGL_EMSCRIPTEN) block.

37-42: -include stdlib.h as INTERFACE on fmt leaks into every consumer.

Propagating -include stdlib.h via INTERFACE forces this flag onto every target that links fmt::fmt (which, per src/sgl/CMakeLists.txt:367-395, is most of SGL and all examples). If the goal is only to fix fmt's own TUs, PRIVATE is sufficient. If the INTERFACE is required because fmt's headers reference malloc/free from templates, a brief comment explaining that would be helpful; otherwise consider dropping the INTERFACE line.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@external/CMakeLists.txt` around lines 37 - 42, The INTERFACE -include
stdlib.h flag on target_compile_options(fmt ...) is leaking into every consumer;
change the second target_compile_options(fmt INTERFACE "-include" "stdlib.h") to
be removed so only the PRIVATE -include is applied (i.e., keep
target_compile_options(fmt PRIVATE "-include" "stdlib.h") only) unless you
actually need consumers to inherit the include—if inheritance is required,
replace the INTERFACE usage with a clear comment explaining why fmt headers
require malloc/free visibility and document that this flag must propagate to
consumers; ensure the code branch under SGL_EMSCRIPTEN only applies the PRIVATE
compile option to fmt.

281-296: FORCE-setting SLANG_RHI_FETCH_SLANG / SLANG_RHI_BUILD_FROM_SLANG_REPO overrides user/CI configuration.

Using CACHE BOOL "" FORCE here unconditionally overwrites whatever the user passed on the command line or any parent-scope configuration, every time CMake is re-run. For the non-Emscripten path (line 294) this is a behavior change from the previous state where slang-rhi's own defaults applied.

If the goal is only to set defaults, prefer plain set(... ) (non-cache) to propagate to the subdirectory, or drop the FORCE so users can override. If the goal really is a hard override for correctness, a short comment explaining why would help.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@external/CMakeLists.txt` around lines 281 - 296, The CMake snippet
unconditionally forces SLANG_RHI_FETCH_SLANG and SLANG_RHI_BUILD_FROM_SLANG_REPO
into the cache (using CACHE BOOL "" FORCE), which overrides user/CI
configuration; change the two FORCE cache sets so they don't clobber user
values: in the SGL_EMSCRIPTEN branch prefer non-cache set(SLANG_RHI_FETCH_SLANG
OFF) / set(SLANG_RHI_BUILD_FROM_SLANG_REPO ON) or use CACHE without FORCE to
provide defaults users can override, and similarly remove FORCE from the else()
set(SLANG_RHI_FETCH_SLANG OFF CACHE BOOL "" ) or replace with a plain set(...)
so the subproject/defaults remain overridable; if you truly need to
hard-override, add a brief comment explaining why and keep FORCE intentionally.

142-146: Unreachable early return.

sgl_add_library_slang() is only invoked when NOT SGL_EMSCRIPTEN OR SGL_LOCAL_SLANG (line 261), so the SGL_EMSCRIPTEN AND NOT SGL_LOCAL_SLANG guard here can never fire. Either drop the early return or drop the outer gate — keeping both makes the control flow harder to follow and hides the intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@external/CMakeLists.txt` around lines 142 - 146, The early return inside
function sgl_add_library_slang checks SGL_EMSCRIPTEN AND NOT SGL_LOCAL_SLANG but
is unreachable because callers only invoke sgl_add_library_slang when that
condition is already false; remove the redundant guard: delete the
if(SGL_EMSCRIPTEN AND NOT SGL_LOCAL_SLANG) ... return() ... endif() block from
sgl_add_library_slang so control flow is governed by the caller, or
alternatively remove the external caller-side gate and keep the function-level
guard — choose one consistent location for the conditional and remove the other
to avoid duplicated/hidden logic.
src/sgl/core/macros.h (1)

54-55: Use SGL_EMSCRIPTEN value check rather than defined().

SGL_EMSCRIPTEN is defined unconditionally above (line 35) as an expression that evaluates to 0 or 1, so defined(SGL_EMSCRIPTEN) is always true. It happens to work here because the preceding #if/#elif branches filter out x86_64/arm64 first, but the intent is clearly a value check. For consistency with the rest of the file (e.g., SGL_CLANG || SGL_GCC at line 119), prefer:

♻️ Proposed change
-#elif defined(SGL_EMSCRIPTEN)
+#elif SGL_EMSCRIPTEN
 `#define` SGL_ARCH SGL_ARCH_WASM
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/core/macros.h` around lines 54 - 55, The conditional uses
defined(SGL_EMSCRIPTEN) but SGL_EMSCRIPTEN is defined earlier as 0/1, so replace
the preprocessor check with a value check: change the branch that sets SGL_ARCH
to SGL_ARCH_WASM to use `#elif` SGL_EMSCRIPTEN (or equivalent truthy evaluation)
instead of defined(SGL_EMSCRIPTEN) so the check matches how other macros (e.g.,
SGL_CLANG || SGL_GCC) are evaluated and correctly honors the 0/1 definition of
SGL_EMSCRIPTEN.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/render_pipeline/render_pipeline.slang`:
- Around line 11-23: The barycentric synthesis using uint id = vid % 3; inside
vertex_main (using vid and id to set output.barycentrics) only yields correct
results for sequential per-triangle vertex IDs (e.g., 0,1,2,0,1,2…) and will be
wrong for indexed draws, strips/fans or non-sequential IDs; update the
vertex_main implementation to either derive barycentrics from a safe
primitive-based source (e.g., SV_PrimitiveID combined with vertex index) when
available or, at minimum, add a clear comment above the uint id = vid % 3; line
explaining this limitation and warning against copy-paste reuse for
indexed/strip/fan draws so future readers know when to replace the hack.

In `@examples/wasm/simple_compute.html`:
- Around line 48-63: Add failure handlers so demo shows errors instead of
staying stuck on "Loading WASM module...": extend the existing Module object
(the same object that defines onRuntimeInitialized/print/printErr) with an
onAbort function that sets status.textContent to a descriptive failure message,
sets status.className to 'status error', and writes the error to output (similar
to printErr); also attach an onerror handler to the <script
src="simple-compute-wasm.js"> element (the script tag that loads the Emscripten
bundle) to set the same status and output text when the file fails to load.
Ensure you reuse the same status/output DOM variables and log the original error
message/URL for debugging.

In `@external/CMakeLists.txt`:
- Around line 313-322: The CMake fetch for slang-rhi uses a moving ref "GIT_TAG
main" which breaks reproducible builds; update the FetchContent_Declare call for
the slang-rhi dependency (the block guarded by SGL_EMSCRIPTEN that calls
FetchContent_Declare and FetchContent_MakeAvailable) to pin GIT_TAG to a
specific commit SHA or an explicit release tag instead of "main" (once the
companion slang-rhi PR/commit is finalized), and document the chosen SHA/tag in
a comment so future bumps are intentional.

In `@src/sgl/CMakeLists.txt`:
- Around line 417-428: The target_link_options block guarded by SGL_EMSCRIPTEN
for target "sgl" uses incompatible and duplicated Emscripten flags; update the
block to remove duplicates and fix the exceptions/Asyncify conflict by replacing
"-fwasm-exceptions" with "-fexceptions" (or alternatively set Asyncify mode to
JSPI via "-sASYNCIFY=2" if you need native wasm EH), and decide one
source-of-truth for flags (either keep them here on target_link_options PUBLIC
for target sgl or remove them from CMakePresets.json so they don't duplicate);
also ensure the PUBLIC propagation of "--no-entry" is intentional for downstream
consumers/examples that expect a no-entry wasm module.

In `@src/sgl/core/file_stream.cpp`:
- Around line 45-46: The Emscripten branch calls strerror(errnum) without
including the header that declares it; add the standard C++ header that provides
strerror (i.e., include <cstring>) at the top of the translation unit so all
branches (strerror, strerror_r, strerror_s) have the proper declaration; update
file_stream.cpp to add the <cstring> include near the other standard headers to
avoid relying on transitive includes.

In `@src/sgl/core/memory_mapped_file.h`:
- Around line 101-102: The Emscripten build path now uses the POSIX-like
FileHandle but memory_mapped_file.cpp references MAP_FAILED (e.g., in the remap
logic), and that macro is undefined because <sys/mman.h> is not included for the
Emscripten branch; fix by adding `#include` <sys/mman.h> to the Emscripten
conditional includes in memory_mapped_file.cpp so MAP_FAILED and related mmap
flags are defined (ensure the include is added alongside the existing
platform-specific includes for the Emscripten branch that uses FileHandle).

In `@src/sgl/core/object.h`:
- Around line 17-22: The typedef for Py_ssize_t_ under SGL_EMSCRIPTEN currently
forces int64_t which mismatches CPython's 32-bit Py_ssize_t on wasm32; change
the typedef to use intptr_t for Py_ssize_t_ when SGL_EMSCRIPTEN is defined (or
alternatively add a static_assert on sizeof(Py_ssize_t_) == sizeof(size_t) to
fail fast) so the width matches the platform; update the block around the
SGL_EMSCRIPTEN conditional that defines Py_ssize_t_ to use intptr_t (or add the
size static_assert) and keep the existing non-Emscripten branch and its
static_assert intact.

In `@src/sgl/core/platform_emscripten.cpp`:
- Around line 1-146: platform.cpp currently unconditionally includes
<GLFW/glfw3.h> and calls display_scale_factor() which uses GLFW, causing
Emscripten builds to fail; fix by making the GLFW dependency platform-gated:
either move GLFW-dependent logic (including display_scale_factor() and any GLFW
includes/uses) into an SGL non-Emscripten-specific source file, or wrap the GLFW
include and the display_scale_factor() implementation in `#if` !SGL_EMSCRIPTEN /
`#endif` guards so the header and GLFW calls are excluded for Emscripten while
preserving other functions like is_python_active() and format_stacktrace().

In `@src/sgl/core/string.cpp`:
- Around line 122-130: The conditional uses "#ifndef SGL_EMSCRIPTEN" which is
wrong because SGL_EMSCRIPTEN is always defined; change the preprocessor check to
use "#if !SGL_EMSCRIPTEN" so the TB branch (the fmt::format("{:.2f} TB", size /
1099511627776.0) return) is included on native builds; update the directive
surrounding the GB/TB returns in src/sgl/core/string.cpp (the block that
currently returns GB in the Emscripten case and TB for large sizes) to use `#if`
!SGL_EMSCRIPTEN / `#else` / `#endif` instead of `#ifndef`.

In `@src/sgl/core/window.cpp`:
- Around line 420-422: The code hardcodes "#canvas" into handle.canvasSelector
for SGL_EMSCRIPTEN; add an optional canvas_selector (or canvasSelector) field to
WindowDesc and make the Window creation/initialization (where
handle.canvasSelector is assigned) use WindowDesc.canvas_selector if present,
otherwise fall back to "#canvas" for backward compatibility; update any
Window/WindowDesc constructors or factory functions that build WindowHandle to
accept and propagate this new field so multiple WASM canvases can be targeted.

In `@src/sgl/device/surface.cpp`:
- Around line 26-27: The Surface(WindowHandle, ref<Device>) constructor passes
window_handle.canvasSelector directly into rhi::WindowHandle::fromWGPUCanvas
without checking for null; add a guard in the Surface constructor to detect a
null or empty canvasSelector on the incoming WindowHandle and substitute a safe
default (e.g. "#canvas") before calling rhi::WindowHandle::fromWGPUCanvas.
Locate the branch for SGL_EMSCRIPTEN in surface.cpp where
rhi::WindowHandle::fromWGPUCanvas(window_handle.canvasSelector) is used and wrap
it so that if window_handle.canvasSelector is null/empty you call fromWGPUCanvas
with the default selector instead.

---

Outside diff comments:
In `@src/sgl/core/file_system_watcher.cpp`:
- Around line 77-148: The directory scan in FileSystemWatcher::add_watch
currently populates state->files via get_directory_files even on Emscripten
where the polling thread (m_thread/thread_func) is not compiled; change the
guard around that assignment so it excludes Emscripten (i.e., only run
state->files = get_directory_files(...) when not SGL_LINUX and not
SGL_EMSCRIPTEN) to avoid doing unnecessary work on Emscripten platforms, keeping
all other logic (id generation, state initialization, and Linux inotify
handling) unchanged.

In `@src/sgl/core/memory_mapped_file.cpp`:
- Around line 201-241: The Emscripten branch is wrongly nested inside the
Linux/macOS conditional so when SGL_EMSCRIPTEN is defined m_mapped_data and
m_mapped_size are never set; move the SGL_EMSCRIPTEN handling out to its own
top-level `#elif` SGL_EMSCRIPTEN branch (parallel to the Windows and Linux/macOS
branches) so it executes independently, set m_mapped_data = MAP_FAILED (or
nullptr equivalent), set/clear m_mapped_size appropriately and return false on
failure; update the branch locations around the existing MapViewOfFile (Windows)
and mmap/madvise (Linux/macOS) code and ensure symbols m_mapped_data,
m_mapped_size, SGL_EMSCRIPTEN, MAP_FAILED, mmap/mmap64, and ::madvise are used
consistently.

In `@src/sgl/device/cuda_utils.cpp`:
- Around line 3-408: At the top of this CUDA implementation (sgl::cuda namespace
/ functions like get_current_device_index and class Device), include the project
config header by adding `#include` "sgl/core/config.h" before the feature guard,
and replace the existing preprocessor check from "#ifdef SGL_HAS_CUDA" to "#if
SGL_HAS_CUDA" (keeping the trailing `#endif` unchanged) so the numeric
CMake-defined macro is tested correctly.

In `@src/sgl/utils/renderdoc.cpp`:
- Around line 5-174: The file uses an `#ifndef` SGL_EMSCRIPTEN guard which is
wrong because SGL_EMSCRIPTEN is defined as 0/1; change the top-level
preprocessor guard to `#if` !SGL_EMSCRIPTEN (and similarly update the matching
endif comment if present) so the renderdoc::API class and its functions (API,
API::start_frame_capture, API::end_frame_capture, is_available,
start_frame_capture, end_frame_capture, is_frame_capturing) are compiled when
not targeting Emscripten; apply the same fix to the other affected translation
units that use the same pattern (renderdoc.h, window.cpp, string.cpp) for
consistency.

---

Nitpick comments:
In `@CMakePresets.json`:
- Around line 201-213: Update the emscripten preset to use
CMAKE_EXE_LINKER_FLAGS_INIT (mirror CMAKE_CXX_FLAGS_INIT) so linker flags remain
override-friendly, remove duplicate flags that are already set at target level
(drop -sUSE_GLFW=3, -sALLOW_MEMORY_GROWTH, -fwasm-exceptions from the preset)
and rely on the existing target_link_options(sgl PUBLIC ...) in
src/sgl/CMakeLists.txt for those options, and add a short note documenting the
non-obvious "-include cstdlib" workaround (e.g., in the preset "description"
field or commit message) so its purpose is recorded for future maintainers.

In `@examples/render_pipeline/render_pipeline.cpp`:
- Around line 130-135: The destructor ~App() currently calls device->close()
before releasing ref-counted members (pipeline, program, input_layout, buffers,
surface), which can break if Device::close() later releases m_rhi_device; update
~App() to first explicitly reset/release all dependent ref-counted members (call
reset() or clear() on pipeline, program, input_layout, surface and any
buffers/containers holding RHI refs) and only after those are cleared call
device->close() (and then release the device itself), so the dependent objects
are destroyed before the device teardown.
- Around line 137-154: The current main loop in main() spins with
emscripten_sleep(0); replace the busy loop by using emscripten_set_main_loop_arg
to run a callback that calls app.main_loop() and checks
app.window->should_close(), calling emscripten_cancel_main_loop() to stop when
needed; alternatively, if you must keep the loop, change emscripten_sleep(0) to
a non-zero value like emscripten_sleep(16) to throttle CPU. Locate the main
function and the App instance (symbols: main, App, app, app.main_loop(),
app.window->should_close()) and implement the main-loop callback (used with
emscripten_set_main_loop_arg) or the increased sleep to align with vsync.

In `@examples/simple_compute/simple_compute.cpp`:
- Around line 15-19: Rename the platform-conditional local constant EXAMPLE_DIR
to snake_case example_dir and switch the preprocessor check to use the project
macro SGL_EMSCRIPTEN; specifically update the branch that currently tests
__EMSCRIPTEN__ to use SGL_EMSCRIPTEN and replace occurrences of EXAMPLE_DIR with
example_dir while preserving the same std::filesystem::path initialization from
SGL_EXAMPLE_DIR or "." in the emscripten case so all uses (e.g., in this file
and the other occurrence at lines ~30) follow the project's naming and macro
conventions.

In `@examples/wasm/CMakeLists.txt`:
- Around line 1-41: Both WASM example targets (render-pipeline-wasm and
simple-compute-wasm) duplicate the same CMake steps; add a helper macro/function
(e.g., add_wasm_example(NAME SOURCE SLANG_HTML_BASENAME)) that performs
add_executable, target_compile_features(... cxx_std_20),
target_link_libraries(... sgl), target_link_options to add the
"SHELL:--preload-file
${CMAKE_CURRENT_SOURCE_DIR}/../<example>/<basename>.slang@<basename>.slang"
entry, target_compile_definitions(SGL_EXAMPLE_DIR="."), and the
add_custom_command POST_BUILD that copies the .slang and .html; update calls for
render-pipeline-wasm and simple-compute-wasm to use this macro and optionally
remove the POST_BUILD copy if you rely solely on --preload-file for the virtual
FS.

In `@external/CMakeLists.txt`:
- Around line 132-139: The inner conditional if(NOT SGL_LOCAL_SLANG) inside the
elseif(NOT SGL_EMSCRIPTEN) branch is redundant because SGL_LOCAL_SLANG is
already false in this branch; remove that inner if/endif and unindent its body
so sgl_download_package(slang ${SLANG_URL}) and the subsequent set(...) calls
(SLANG_DIR, SLANG_INCLUDE_DIR) execute directly within the elseif(NOT
SGL_EMSCRIPTEN) block.
- Around line 37-42: The INTERFACE -include stdlib.h flag on
target_compile_options(fmt ...) is leaking into every consumer; change the
second target_compile_options(fmt INTERFACE "-include" "stdlib.h") to be removed
so only the PRIVATE -include is applied (i.e., keep target_compile_options(fmt
PRIVATE "-include" "stdlib.h") only) unless you actually need consumers to
inherit the include—if inheritance is required, replace the INTERFACE usage with
a clear comment explaining why fmt headers require malloc/free visibility and
document that this flag must propagate to consumers; ensure the code branch
under SGL_EMSCRIPTEN only applies the PRIVATE compile option to fmt.
- Around line 281-296: The CMake snippet unconditionally forces
SLANG_RHI_FETCH_SLANG and SLANG_RHI_BUILD_FROM_SLANG_REPO into the cache (using
CACHE BOOL "" FORCE), which overrides user/CI configuration; change the two
FORCE cache sets so they don't clobber user values: in the SGL_EMSCRIPTEN branch
prefer non-cache set(SLANG_RHI_FETCH_SLANG OFF) /
set(SLANG_RHI_BUILD_FROM_SLANG_REPO ON) or use CACHE without FORCE to provide
defaults users can override, and similarly remove FORCE from the else()
set(SLANG_RHI_FETCH_SLANG OFF CACHE BOOL "" ) or replace with a plain set(...)
so the subproject/defaults remain overridable; if you truly need to
hard-override, add a brief comment explaining why and keep FORCE intentionally.
- Around line 142-146: The early return inside function sgl_add_library_slang
checks SGL_EMSCRIPTEN AND NOT SGL_LOCAL_SLANG but is unreachable because callers
only invoke sgl_add_library_slang when that condition is already false; remove
the redundant guard: delete the if(SGL_EMSCRIPTEN AND NOT SGL_LOCAL_SLANG) ...
return() ... endif() block from sgl_add_library_slang so control flow is
governed by the caller, or alternatively remove the external caller-side gate
and keep the function-level guard — choose one consistent location for the
conditional and remove the other to avoid duplicated/hidden logic.

In `@src/sgl/core/macros.h`:
- Around line 54-55: The conditional uses defined(SGL_EMSCRIPTEN) but
SGL_EMSCRIPTEN is defined earlier as 0/1, so replace the preprocessor check with
a value check: change the branch that sets SGL_ARCH to SGL_ARCH_WASM to use
`#elif` SGL_EMSCRIPTEN (or equivalent truthy evaluation) instead of
defined(SGL_EMSCRIPTEN) so the check matches how other macros (e.g., SGL_CLANG
|| SGL_GCC) are evaluated and correctly honors the 0/1 definition of
SGL_EMSCRIPTEN.

In `@src/sgl/core/platform.h`:
- Around line 33-34: Rename the public field canvasSelector to snake_case
canvas_selector and add a Doxygen triple-slash comment (///) describing the
field in the struct/class guarded by SGL_EMSCRIPTEN; update its initializer in
the window initialization code that sets handle.canvasSelector to use
handle.canvas_selector, and update the consumer in the surface code that reads
window_handle.canvasSelector to use window_handle.canvas_selector; ensure all
references and includes compile after the rename and that the Doxygen comment
describes the purpose (e.g., selector for the Emscripten canvas element).

In `@src/sgl/core/thread.h`:
- Around line 8-10: Replace the compiler macro check __EMSCRIPTEN__ with the
project macro SGL_EMSCRIPTEN in the conditional include at the top of thread.h
so it matches the rest of the file and the project's convention; specifically
change the `#ifndef/__EMSCRIPTEN__` guard surrounding the nanothread include to
use SGL_EMSCRIPTEN (the file already includes sgl/core/macros.h above), keeping
the include and surrounding preprocessor structure identical otherwise.
- Around line 21-130: The duplicated blocked_range<Int> definition should be
hoisted out of the SGL_EMSCRIPTEN conditional: move the blocked_range template
(including its iterator, blocks(), begin()/end(), block_size() and private
members) above the `#ifdef` SGL_EMSCRIPTEN so both Emscripten and non-Emscripten
paths reuse it; leave the Emscripten mocks for do_async, parallel_for,
parallel_for_async, TaskGroup, and task_* wrappers as-is, and do not add unused
nanothread symbols (task_query, task_time, task_time_rel) to the Emscripten
section since they aren’t referenced.

In `@src/sgl/device/device.cpp`:
- Around line 73-93: Replace uses of SLANG_WASM in src/sgl/device/device.cpp
with the project-standard SGL_EMSCRIPTEN: change the first preprocessor guard
from `#if` !SLANG_WASM to `#if` !SGL_EMSCRIPTEN (around the
setDownstreamCompilerPath loop) and change the automatic device-type branch from
`#if` SLANG_WASM to `#if` SGL_EMSCRIPTEN (the block that sets m_desc.type =
DeviceType::wgpu). Ensure SGL_EMSCRIPTEN is available in this translation unit
(include the header that defines it, e.g., sgl/core/macros.h) if not already
included.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3852d29b-9f63-475e-bc74-954024463cb8

📥 Commits

Reviewing files that changed from the base of the PR and between 3d813d6 and eb0fbea.

📒 Files selected for processing (36)
  • .gitignore
  • CMakeLists.txt
  • CMakePresets.json
  • examples/CMakeLists.txt
  • examples/render_pipeline/render_pipeline.cpp
  • examples/render_pipeline/render_pipeline.slang
  • examples/simple_compute/simple_compute.cpp
  • examples/wasm/CMakeLists.txt
  • examples/wasm/render_pipeline.html
  • examples/wasm/simple_compute.html
  • external/CMakeLists.txt
  • src/sgl/CMakeLists.txt
  • src/sgl/core/file_stream.cpp
  • src/sgl/core/file_system_watcher.cpp
  • src/sgl/core/file_system_watcher.h
  • src/sgl/core/lmdb_cache.cpp
  • src/sgl/core/macros.h
  • src/sgl/core/memory_mapped_file.cpp
  • src/sgl/core/memory_mapped_file.h
  • src/sgl/core/object.h
  • src/sgl/core/platform.h
  • src/sgl/core/platform_emscripten.cpp
  • src/sgl/core/string.cpp
  • src/sgl/core/thread.h
  • src/sgl/core/window.cpp
  • src/sgl/device/cuda_utils.cpp
  • src/sgl/device/device.cpp
  • src/sgl/device/device.h
  • src/sgl/device/native_handle_traits.h
  • src/sgl/device/shader_cursor.cpp
  • src/sgl/device/shader_cursor.h
  • src/sgl/device/shader_object.cpp
  • src/sgl/device/shader_object.h
  • src/sgl/device/surface.cpp
  • src/sgl/utils/renderdoc.cpp
  • src/sgl/utils/renderdoc.h

Comment on lines +11 to +23
[shader("vertex")]
VertexOutput vertex_main(float2 pos: POSITION, uint vid: SV_VertexID)
{
VertexOutput output;
output.pos = float4(pos, 0.0, 1.0);

// Mimic SV_Barycentrics for backends that don't support it (e.g., WebGPU)
uint id = vid % 3;
output.barycentrics = float3(0, 0, 0);
output.barycentrics[id] = 1.0;

return output;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Barycentric synthesis via vid % 3 is only valid for this specific draw.

uint id = vid % 3; produces correct barycentrics only when the provided vertex IDs are sequential within each triangle (0,1,2,0,1,2,…). For indexed draws with arbitrary indices, or for triangle strips/fans, this will produce wrong results. That's fine for this one-triangle example, but please add a comment saying so to prevent copy-paste reuse, or derive it from SV_PrimitiveID/SV_VertexID only when safe.

As per coding guidelines (Slang shader code must use [shader("compute")] attribute for GPU entry points): note this applies to compute entry points — the [shader("vertex")] / [shader("fragment")] attributes here are correct for raster stages, no change needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/render_pipeline/render_pipeline.slang` around lines 11 - 23, The
barycentric synthesis using uint id = vid % 3; inside vertex_main (using vid and
id to set output.barycentrics) only yields correct results for sequential
per-triangle vertex IDs (e.g., 0,1,2,0,1,2…) and will be wrong for indexed
draws, strips/fans or non-sequential IDs; update the vertex_main implementation
to either derive barycentrics from a safe primitive-based source (e.g.,
SV_PrimitiveID combined with vertex index) when available or, at minimum, add a
clear comment above the uint id = vid % 3; line explaining this limitation and
warning against copy-paste reuse for indexed/strip/fan draws so future readers
know when to replace the hack.

Comment thread examples/wasm/simple_compute.html Outdated
Comment on lines +48 to +63
var Module = {
onRuntimeInitialized: function() {
status.textContent = '✓ Simple Compute WASM module loaded successfully!';
status.className = 'status success';
},
print: function(text) {
console.log(text);
output.textContent += text + '\n';
},
printErr: function(text) {
console.error(text);
output.textContent += '[ERROR] ' + text + '\n';
}
};
</script>
<script src="simple-compute-wasm.js"></script>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Set the demo status on load/initialization failures.

Right now a missing simple-compute-wasm.js or Emscripten abort leaves the page stuck on “Loading WASM module...”. Add onAbort and a script onerror handler so browser demo failures are visible.

Proposed fix
         var Module = {
             onRuntimeInitialized: function() {
                 status.textContent = '✓ Simple Compute WASM module loaded successfully!';
                 status.className = 'status success';
             },
+            onAbort: function(reason) {
+                status.textContent = '✗ Simple Compute WASM module failed to initialize';
+                status.className = 'status error';
+                output.textContent += '[ERROR] ' + reason + '\n';
+            },
             print: function(text) {
                 console.log(text);
                 output.textContent += text + '\n';
             },
             printErr: function(text) {
                 console.error(text);
                 output.textContent += '[ERROR] ' + text + '\n';
             }
         };
     </script>
-    <script src="simple-compute-wasm.js"></script>
+    <script
+        src="simple-compute-wasm.js"
+        onerror="status.textContent='✗ Failed to load simple-compute-wasm.js'; status.className='status error';">
+    </script>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var Module = {
onRuntimeInitialized: function() {
status.textContent = '✓ Simple Compute WASM module loaded successfully!';
status.className = 'status success';
},
print: function(text) {
console.log(text);
output.textContent += text + '\n';
},
printErr: function(text) {
console.error(text);
output.textContent += '[ERROR] ' + text + '\n';
}
};
</script>
<script src="simple-compute-wasm.js"></script>
var Module = {
onRuntimeInitialized: function() {
status.textContent = '✓ Simple Compute WASM module loaded successfully!';
status.className = 'status success';
},
onAbort: function(reason) {
status.textContent = '✗ Simple Compute WASM module failed to initialize';
status.className = 'status error';
output.textContent += '[ERROR] ' + reason + '\n';
},
print: function(text) {
console.log(text);
output.textContent += text + '\n';
},
printErr: function(text) {
console.error(text);
output.textContent += '[ERROR] ' + text + '\n';
}
};
</script>
<script
src="simple-compute-wasm.js"
onerror="status.textContent='✗ Failed to load simple-compute-wasm.js'; status.className='status error';">
</script>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/wasm/simple_compute.html` around lines 48 - 63, Add failure handlers
so demo shows errors instead of staying stuck on "Loading WASM module...":
extend the existing Module object (the same object that defines
onRuntimeInitialized/print/printErr) with an onAbort function that sets
status.textContent to a descriptive failure message, sets status.className to
'status error', and writes the error to output (similar to printErr); also
attach an onerror handler to the <script src="simple-compute-wasm.js"> element
(the script tag that loads the Emscripten bundle) to set the same status and
output text when the file fails to load. Ensure you reuse the same status/output
DOM variables and log the original error message/URL for debugging.

Comment thread external/CMakeLists.txt Outdated
Comment on lines +313 to +322
elseif(SGL_EMSCRIPTEN)
# Emscripten uses upstream slang-rhi directly here because the checked-in
# submodule may still be pinned to an older commit.
FetchContent_Declare(
slang-rhi
GIT_REPOSITORY https://github.com/shader-slang/slang-rhi.git
GIT_TAG main
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(slang-rhi)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

GIT_TAG main breaks build reproducibility.

Fetching slang-rhi at main means every clean configure may pull a different commit, so two developers (or CI runs a day apart) can end up with silently different SGL behavior — and bisection becomes unreliable. Please pin to a specific commit SHA (or a tag) once the companion slang-rhi PR is merged, and bump it intentionally.

♻️ Suggested change
     FetchContent_Declare(
         slang-rhi
         GIT_REPOSITORY https://github.com/shader-slang/slang-rhi.git
-        GIT_TAG main
+        GIT_TAG <pinned-commit-sha>
         GIT_SHALLOW TRUE
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@external/CMakeLists.txt` around lines 313 - 322, The CMake fetch for
slang-rhi uses a moving ref "GIT_TAG main" which breaks reproducible builds;
update the FetchContent_Declare call for the slang-rhi dependency (the block
guarded by SGL_EMSCRIPTEN that calls FetchContent_Declare and
FetchContent_MakeAvailable) to pin GIT_TAG to a specific commit SHA or an
explicit release tag instead of "main" (once the companion slang-rhi PR/commit
is finalized), and document the chosen SHA/tag in a comment so future bumps are
intentional.

Comment thread src/sgl/CMakeLists.txt
Comment on lines +417 to +428
if(SGL_EMSCRIPTEN)
target_link_options(sgl
PUBLIC
"-sUSE_GLFW=3"
"--use-port=emdawnwebgpu"
"-fwasm-exceptions"
"-sASYNCIFY=1"
"-sALLOW_MEMORY_GROWTH=1"
"-sMAXIMUM_MEMORY=4GB"
"--no-entry"
)
endif()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does Emscripten support combining -fwasm-exceptions with -sASYNCIFY=1? Any required additional flags or caveats?

💡 Result:

No, Emscripten does not fully support combining -fwasm-exceptions with -sASYNCIFY=1. The compiler emits a warning: "ASYNCIFY=1 is not compatible with -fwasm-exceptions. Parts of the program that mix ASYNCIFY and exceptions will not compile." Builds may succeed but fail at runtime or during wasm-opt if code mixes async suspend points and exception handling (try-catch blocks), due to incomplete Binaryen support for transforming Wasm EH try constructs under Asyncify. Partial upstream support exists in Binaryen since 2023, but Emscripten warns due to reliability issues. Use -fexceptions (JS-based) with ASYNCIFY instead, or JSPI (-sASYNCIFY=2) with -fwasm-exceptions for better compatibility. No additional flags resolve the core incompatibility. As of Emscripten 5.0.6-git (2026), official docs do not list full support, and changelog lacks confirmation.

Citations:


🏁 Script executed:

#!/bin/bash
# Find and inspect CMakePresets.json for Emscripten-related flags
find . -name "CMakePresets.json" -type f

Repository: shader-slang/slangpy

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Check CMakePresets.json for Emscripten preset and linker flags
cat CMakePresets.json | head -200

Repository: shader-slang/slangpy

Length of output: 6311


🏁 Script executed:

#!/bin/bash
# Search for Emscripten preset and its linker flags configuration
rg -A 30 '"name".*emscripten' CMakePresets.json -i

Repository: shader-slang/slangpy

Length of output: 1996


Fix incompatible Emscripten link flags and consolidate duplicates.

Three issues with the Emscripten target_link_options block:

  1. Critical: -fwasm-exceptions and -sASYNCIFY=1 are incompatible. Emscripten emits a warning: "ASYNCIFY=1 is not compatible with -fwasm-exceptions." Code mixing async suspend points and exception handling will fail at runtime or during wasm-opt optimization. Replace -fwasm-exceptions with -fexceptions (JS-based exceptions) to keep Asyncify, or switch to JSPI (-sASYNCIFY=2) if you need native wasm EH.

  2. -sUSE_GLFW=3, -fwasm-exceptions, and -sALLOW_MEMORY_GROWTH are already set in CMakePresets.json (CMAKE_EXE_LINKER_FLAGS). Duplicated link flags make the final command line noisy and allow drift. Keep one source of truth—either the target-level options here (PUBLIC propagates to all consumers) and remove overlap from the preset, or vice versa.

  3. --no-entry is correct for a STATIC library on Emscripten, but it will propagate PUBLICly to anything linking sgl. Ensure all example targets expect a no-entry/wasm-module layout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/CMakeLists.txt` around lines 417 - 428, The target_link_options block
guarded by SGL_EMSCRIPTEN for target "sgl" uses incompatible and duplicated
Emscripten flags; update the block to remove duplicates and fix the
exceptions/Asyncify conflict by replacing "-fwasm-exceptions" with
"-fexceptions" (or alternatively set Asyncify mode to JSPI via "-sASYNCIFY=2" if
you need native wasm EH), and decide one source-of-truth for flags (either keep
them here on target_link_options PUBLIC for target sgl or remove them from
CMakePresets.json so they don't duplicate); also ensure the PUBLIC propagation
of "--no-entry" is intentional for downstream consumers/examples that expect a
no-entry wasm module.

Comment on lines +45 to +46
#elif SGL_EMSCRIPTEN
return strerror(errnum);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -i '^file_stream\.cpp$' --exec sh -c '
  printf "\n== %s ==\n" "$1"
  sed -n "1,55p" "$1"
' sh {}

rg -n -C2 '\b(strerror|strerror_r|errno)\b' --iglob 'file_stream.cpp'

Repository: shader-slang/slangpy

Length of output: 4225


Add the header declaring strerror().

The Emscripten branch (line 46) calls strerror() without a direct include for the header that declares it. All platform branches use string error functions (strerror(), strerror_r(), strerror_s()), which require <cstring> to be declared. Relying on transitive includes is fragile and may break with toolchain changes.

🔧 Proposed include fix
 `#include` "sgl/core/file_stream.h"
 
 `#include` "sgl/core/error.h"
 
+#include <cstring>
 `#include` <fstream>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/core/file_stream.cpp` around lines 45 - 46, The Emscripten branch
calls strerror(errnum) without including the header that declares it; add the
standard C++ header that provides strerror (i.e., include <cstring>) at the top
of the translation unit so all branches (strerror, strerror_r, strerror_s) have
the proper declaration; update file_stream.cpp to add the <cstring> include near
the other standard headers to avoid relying on transitive includes.

Comment thread src/sgl/core/object.h Outdated
Comment on lines +17 to +22
#if SGL_EMSCRIPTEN
typedef int64_t Py_ssize_t_;
#else
typedef intptr_t Py_ssize_t_;
static_assert(sizeof(Py_ssize_t_) == sizeof(size_t));
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

On wasm32 Emscripten builds (including pyodide), is CPython's Py_ssize_t 32-bit or 64-bit?

💡 Result:

On wasm32 Emscripten builds (including Pyodide), CPython's Py_ssize_t is 32-bit.

Citations:


🏁 Script executed:

#!/bin/bash
# Search for SGL_EMSCRIPTEN usage and context
rg "SGL_EMSCRIPTEN" --type cpp --type h -B 3 -A 3

# Search for any wasm64 or build documentation
rg "wasm64|MEMORY64" --type cpp --type h --type py --type txt

# Check if there are any comments about Py_ssize_t in the codebase
rg "Py_ssize_t" --type cpp --type h -B 2 -A 2

Repository: shader-slang/slangpy

Length of output: 20259


Use intptr_t or add static_assert to catch Emscripten size mismatch.

On wasm32 (the standard Emscripten target, incl. pyodide), CPython's Py_ssize_t is 32-bit. Typedefing Py_ssize_t_ as int64_t here creates a width mismatch that will break Python bindings if ever wired on Emscripten. The codebase shows no wasm64 support, making int64_t appear incorrect. Either change it to intptr_t (which will be 32-bit and match CPython on wasm32) or add the missing size assertion to expose the mismatch at compile time.

Suggested fix
 `#if` SGL_EMSCRIPTEN
-typedef int64_t Py_ssize_t_;
+typedef intptr_t Py_ssize_t_;
+static_assert(sizeof(Py_ssize_t_) == sizeof(size_t));
 `#else`
 typedef intptr_t Py_ssize_t_;
 static_assert(sizeof(Py_ssize_t_) == sizeof(size_t));
 `#endif`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#if SGL_EMSCRIPTEN
typedef int64_t Py_ssize_t_;
#else
typedef intptr_t Py_ssize_t_;
static_assert(sizeof(Py_ssize_t_) == sizeof(size_t));
#endif
`#if` SGL_EMSCRIPTEN
typedef intptr_t Py_ssize_t_;
static_assert(sizeof(Py_ssize_t_) == sizeof(size_t));
`#else`
typedef intptr_t Py_ssize_t_;
static_assert(sizeof(Py_ssize_t_) == sizeof(size_t));
`#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/core/object.h` around lines 17 - 22, The typedef for Py_ssize_t_
under SGL_EMSCRIPTEN currently forces int64_t which mismatches CPython's 32-bit
Py_ssize_t on wasm32; change the typedef to use intptr_t for Py_ssize_t_ when
SGL_EMSCRIPTEN is defined (or alternatively add a static_assert on
sizeof(Py_ssize_t_) == sizeof(size_t) to fail fast) so the width matches the
platform; update the block around the SGL_EMSCRIPTEN conditional that defines
Py_ssize_t_ to use intptr_t (or add the size static_assert) and keep the
existing non-Emscripten branch and its static_assert intact.

Comment on lines +1 to +146
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include "platform.h"

#if SGL_EMSCRIPTEN

#include "sgl/core/error.h"
#include <iostream>

namespace sgl::platform {

void static_init() { }

void static_shutdown() { }

void set_window_icon(WindowHandle handle, const std::filesystem::path& path)
{
SGL_UNUSED(handle);
SGL_UNUSED(path);
}

void set_keyboard_interrupt_handler(std::function<void()> handler)
{
SGL_UNUSED(handler);
}

std::optional<std::filesystem::path> open_file_dialog(std::span<const FileDialogFilter> filters)
{
SGL_UNUSED(filters);
return {};
}

std::optional<std::filesystem::path> save_file_dialog(std::span<const FileDialogFilter> filters)
{
SGL_UNUSED(filters);
return {};
}

std::optional<std::filesystem::path> choose_folder_dialog()
{
return {};
}

bool create_junction(const std::filesystem::path& link, const std::filesystem::path& target)
{
SGL_UNUSED(link);
SGL_UNUSED(target);
return false;
}

bool delete_junction(const std::filesystem::path& link)
{
SGL_UNUSED(link);
return false;
}

const std::filesystem::path& executable_path()
{
static std::filesystem::path path("/");
return path;
}

const std::filesystem::path& app_data_directory()
{
static std::filesystem::path path("/");
return path;
}

const std::filesystem::path& home_directory()
{
static std::filesystem::path path("/");
return path;
}

const std::filesystem::path& runtime_directory()
{
static std::filesystem::path path("/");
return path;
}

std::optional<std::string> get_environment_variable(const char* name)
{
SGL_UNUSED(name);
return std::nullopt;
}

ProcessID current_process_id()
{
return 0;
}

size_t page_size()
{
return 4096;
}

MemoryStats memory_stats()
{
return {0, 0};
}

SharedLibraryHandle load_shared_library(const std::filesystem::path& path)
{
SGL_UNUSED(path);
return nullptr;
}

void release_shared_library(SharedLibraryHandle library)
{
SGL_UNUSED(library);
}

void* get_proc_address(SharedLibraryHandle library, const char* proc_name)
{
SGL_UNUSED(library);
SGL_UNUSED(proc_name);
return nullptr;
}

bool is_debugger_present()
{
return false;
}

void debug_break() { }

void print_to_debug_window(const char* str)
{
std::cerr << str;
}

StackTrace backtrace(size_t skip_frames)
{
SGL_UNUSED(skip_frames);
return {};
}

ResolvedStackTrace resolve_stacktrace(std::span<const StackFrame> trace)
{
SGL_UNUSED(trace);
return {};
}

} // namespace sgl::platform

#endif // SGL_EMSCRIPTEN

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1. Confirm platform.cpp participates in the Emscripten build (no top-level `#if` !SGL_EMSCRIPTEN wrapping everything).
fd -t f 'platform(_.+)?\.cpp' src/sgl/core
echo '--- top of platform.cpp ---'
fd -t f '^platform\.cpp$' src/sgl/core --exec head -n 40 {}
echo '--- CMakeLists for src/sgl ---'
rg -n 'platform(_emscripten)?\.cpp' src/sgl/CMakeLists.txt

# 2. Enumerate public sgl::platform function declarations in platform.h and compare against what's implemented here.
rg -nP '^\s*(SGL_API\s+)?[\w:<>,\s\*&]+\s+\w+\s*\([^;{]*\)\s*;' src/sgl/core/platform.h | rg -v '^\s*//'

# 3. Spot-check references used from error.cpp and logger.cpp.
rg -nP 'platform::(is_python_active|format_stacktrace|backtrace|resolve_stacktrace|get_environment_variable)\b' --type=cpp

Repository: shader-slang/slangpy

Length of output: 2535


🏁 Script executed:

#!/bin/bash
# Find where format_stacktrace is defined
echo "=== Searching for format_stacktrace definition ==="
rg -n 'format_stacktrace\s*\(' src/sgl/core --type=cpp -A 2 | head -30

# Check if platform.cpp has conditional guards for platform-specific code
echo -e "\n=== Checking for guards in platform.cpp (search for GLFW or `#if`) ==="
rg -n '(`#if`|#ifdef|GLFW)' src/sgl/core/platform.cpp | head -20

# Check CMakeLists.txt for full context on how platform files are conditionally included
echo -e "\n=== CMakeLists.txt context around platform files ==="
rg -n -B 5 -A 5 'platform_emscripten\.cpp' src/sgl/CMakeLists.txt

Repository: shader-slang/slangpy

Length of output: 2237


🏁 Script executed:

#!/bin/bash
# Check if CMakeLists uses if() blocks around source file selection
echo "=== Full CMakeLists structure (lines 40-80) ==="
sed -n '40,80p' src/sgl/CMakeLists.txt

# Also check for conditional variables or platform detection
echo -e "\n=== Search for conditional file list (if/endif patterns) ==="
rg -n 'if\s*\(|endif\s*\(' src/sgl/CMakeLists.txt | head -20

# Look for Emscripten or platform-specific conditionals
echo -e "\n=== Search for EMSCRIPTEN or platform checks ==="
rg -n 'EMSCRIPTEN|MINGW|MSVC|APPLE' src/sgl/CMakeLists.txt

Repository: shader-slang/slangpy

Length of output: 2332


🏁 Script executed:

#!/bin/bash
# Check entire platform.cpp for `#if` guards and structure
echo "=== platform.cpp with line numbers (first 100 lines) ==="
head -n 100 src/sgl/core/platform.cpp | cat -n

echo -e "\n=== Search for `#if/`#else/#endif patterns in platform.cpp ==="
rg -n '#if|#else|#endif' src/sgl/core/platform.cpp

Repository: shader-slang/slangpy

Length of output: 3249


🏁 Script executed:

#!/bin/bash
# Get full platform.cpp line count and check for actual `#if` guards
echo "=== Total lines in platform.cpp ==="
wc -l src/sgl/core/platform.cpp

echo -e "\n=== Check for any `#if` guard patterns anywhere in file ==="
grep -n '#if' src/sgl/core/platform.cpp

# Also check if platform.h might have guards around GLFW
echo -e "\n=== Check platform.h for GLFW guards or conditional ==="
grep -n 'GLFW\|#if' src/sgl/core/platform.h | head -20

# Look at what functions are actually in platform_emscripten vs platform.cpp
echo -e "\n=== Functions defined in platform_emscripten.cpp ==="
rg -n '^\w+\s+\w+\s*\(' src/sgl/core/platform_emscripten.cpp | grep -v '//'

echo -e "\n=== Functions defined in platform.cpp (sampling) ==="
rg -n '^\w+\s+\w+\s*\(' src/sgl/core/platform.cpp | head -15

Repository: shader-slang/slangpy

Length of output: 1472


🏁 Script executed:

#!/bin/bash
# Check if platform.cpp itself is conditionally included in CMakeLists
echo "=== Full source list context in CMakeLists (lines 30-90) ==="
sed -n '30,90p' src/sgl/CMakeLists.txt

# Check which functions from platform.cpp are actually called from Emscripten code
echo -e "\n=== Look for calls to functions defined in platform.cpp ==="
rg -n 'is_python_active|set_python_active|display_scale_factor|format_stacktrace' src/sgl/core --type=cpp

# Summary: verify what gets compiled
echo -e "\n=== Verify: is platform.cpp inside or outside the source list? ==="
sed -n '35,65p' src/sgl/CMakeLists.txt | grep -E 'platform|add_library'

Repository: shader-slang/slangpy

Length of output: 3479


platform.cpp unconditionally includes GLFW, which will cause compilation to fail on Emscripten.

platform.cpp is always compiled (it's not conditionally excluded in CMakeLists.txt), but it unconditionally includes <GLFW/glfw3.h> (line 7) with unguarded usage in display_scale_factor() (lines 30–32). Since GLFW is excluded from the Emscripten build (CMakeLists.txt line 379: $<$<NOT:$<BOOL:${SGL_EMSCRIPTEN}>>:glfw>), the compilation will fail with "glfw3.h: No such file or directory" when targeting WebAssembly.

Functions like is_python_active() and format_stacktrace() are correctly defined in platform.cpp, so they are available; however, the GLFW dependency must be resolved. Either move the GLFW-dependent functions to a platform-specific file or wrap the GLFW #include and usage in conditional compilation guards (e.g., #if !SGL_EMSCRIPTEN).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/core/platform_emscripten.cpp` around lines 1 - 146, platform.cpp
currently unconditionally includes <GLFW/glfw3.h> and calls
display_scale_factor() which uses GLFW, causing Emscripten builds to fail; fix
by making the GLFW dependency platform-gated: either move GLFW-dependent logic
(including display_scale_factor() and any GLFW includes/uses) into an SGL
non-Emscripten-specific source file, or wrap the GLFW include and the
display_scale_factor() implementation in `#if` !SGL_EMSCRIPTEN / `#endif` guards so
the header and GLFW calls are excluded for Emscripten while preserving other
functions like is_python_active() and format_stacktrace().

Comment thread src/sgl/core/string.cpp Outdated
Comment on lines +122 to +130
#ifndef SGL_EMSCRIPTEN
else if (size < 1099511627776ull)
return fmt::format("{:.2f} GB", size / 1073741824.0);
else
return fmt::format("{:.2f} TB", size / 1099511627776.0);
#else
else
return fmt::format("{:.2f} GB", size / 1073741824.0);
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm SGL_EMSCRIPTEN is a numeric macro and locate `#ifndef` uses.
# Expected: Numeric 0/1 definition; any `#ifndef` SGL_EMSCRIPTEN sites should be converted to `#if` !SGL_EMSCRIPTEN.

rg -n -C3 '#\s*(define|cmakedefine01)\s+SGL_EMSCRIPTEN|#\s*ifndef\s+SGL_EMSCRIPTEN|#\s*if\s+!SGL_EMSCRIPTEN'

Repository: shader-slang/slangpy

Length of output: 3236


Use #if !SGL_EMSCRIPTEN instead of #ifndef SGL_EMSCRIPTEN.

Since SGL_EMSCRIPTEN is always defined (as a boolean expression in macros.h), the #ifndef directive will never evaluate the macro's value and will always skip the TB formatting code. Use #if !SGL_EMSCRIPTEN to conditionally include TB support on native (non-Emscripten) builds.

Proposed fix
-#ifndef SGL_EMSCRIPTEN
+#if !SGL_EMSCRIPTEN
     else if (size < 1099511627776ull)
         return fmt::format("{:.2f} GB", size / 1073741824.0);
     else
         return fmt::format("{:.2f} TB", size / 1099511627776.0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/core/string.cpp` around lines 122 - 130, The conditional uses
"#ifndef SGL_EMSCRIPTEN" which is wrong because SGL_EMSCRIPTEN is always
defined; change the preprocessor check to use "#if !SGL_EMSCRIPTEN" so the TB
branch (the fmt::format("{:.2f} TB", size / 1099511627776.0) return) is included
on native builds; update the directive surrounding the GB/TB returns in
src/sgl/core/string.cpp (the block that currently returns GB in the Emscripten
case and TB for large sizes) to use `#if` !SGL_EMSCRIPTEN / `#else` / `#endif` instead
of `#ifndef`.

Comment thread src/sgl/core/window.cpp
Comment on lines +420 to 422
#elif SGL_EMSCRIPTEN
handle.canvasSelector = "#canvas";
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded "#canvas" selector couples SGL to a fixed DOM id.

This ties every Window instance in a WASM build to <element id="canvas"> in the host page (as set up in examples/wasm/render_pipeline.html). It precludes multiple windows and makes the example HTML's id a load-bearing contract with the library.

Consider threading the selector through WindowDesc (e.g. a new optional canvas_selector field or window_handle override) so applications can choose their own canvas element, defaulting to "#canvas" for backwards compatibility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/core/window.cpp` around lines 420 - 422, The code hardcodes "#canvas"
into handle.canvasSelector for SGL_EMSCRIPTEN; add an optional canvas_selector
(or canvasSelector) field to WindowDesc and make the Window
creation/initialization (where handle.canvasSelector is assigned) use
WindowDesc.canvas_selector if present, otherwise fall back to "#canvas" for
backward compatibility; update any Window/WindowDesc constructors or factory
functions that build WindowHandle to accept and propagate this new field so
multiple WASM canvases can be targeted.

Comment thread src/sgl/device/surface.cpp Outdated
Comment on lines +26 to +27
#elif SGL_EMSCRIPTEN
rhi::WindowHandle rhi_window_handle = rhi::WindowHandle::fromWGPUCanvas(window_handle.canvasSelector);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the surface.cpp file around lines 26-27
cat -n src/sgl/device/surface.cpp | sed -n '20,35p'

Repository: shader-slang/slangpy

Length of output: 1052


🏁 Script executed:

# Find where rhi::WindowHandle and fromWGPUCanvas are defined
find . -type f \( -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) | xargs grep -l "fromWGPUCanvas" | head -20

Repository: shader-slang/slangpy

Length of output: 92


🏁 Script executed:

# Search for the WindowHandle class definition
find . -type f \( -name "*.h" -o -name "*.hpp" \) | xargs grep -l "class WindowHandle\|struct WindowHandle" | head -10

Repository: shader-slang/slangpy

Length of output: 89


🏁 Script executed:

# Look for rhi namespace and WindowHandle
rg "fromWGPUCanvas" -A 10 -B 2

Repository: shader-slang/slangpy

Length of output: 1160


🏁 Script executed:

# Look for WindowHandle in rhi (likely in slang or external library)
rg "class.*WindowHandle|struct.*WindowHandle" -B 2 -A 15 --type cpp --type h

Repository: shader-slang/slangpy

Length of output: 2024


🏁 Script executed:

# Check platform.h for WindowHandle structure
cat -n src/sgl/core/platform.h | head -100

Repository: shader-slang/slangpy

Length of output: 3527


🏁 Script executed:

# Search for rhi:: namespace definitions
find . -type f \( -name "*.h" -o -name "*.hpp" \) -path "*/rhi/*" | head -20

Repository: shader-slang/slangpy

Length of output: 46


🏁 Script executed:

# Look for fromWGPUCanvas implementation more broadly
rg "fromWGPUCanvas" --type cpp --type h -B 5 -A 15

Repository: shader-slang/slangpy

Length of output: 1640


🏁 Script executed:

# Check if Slang is a git submodule
cat .gitmodules 2>/dev/null || echo "No .gitmodules found"

Repository: shader-slang/slangpy

Length of output: 977


🏁 Script executed:

# Look for Slang RHI headers in the repo
find . -type d -name "rhi" | head -20

Repository: shader-slang/slangpy

Length of output: 46


🏁 Script executed:

# Check if there's a Slang source directory
ls -la | grep -i slang || echo "No obvious slang directory"

Repository: shader-slang/slangpy

Length of output: 123


🏁 Script executed:

# Search for where rhi:: is used and any related includes
rg "rhi::" -B 5 --type cpp | head -100

Repository: shader-slang/slangpy

Length of output: 5627


🏁 Script executed:

# Check surface.cpp includes to understand rhi dependency
head -30 src/sgl/device/surface.cpp

Repository: shader-slang/slangpy

Length of output: 1064


🏁 Script executed:

# Check if slang-rhi submodule is present and look for WindowHandle
ls -la external/slang-rhi/

Repository: shader-slang/slangpy

Length of output: 174


🏁 Script executed:

# Search for fromWGPUCanvas in slang-rhi
find external/slang-rhi -type f \( -name "*.h" -o -name "*.hpp" \) -o -name "*.cpp" | head -20

Repository: shader-slang/slangpy

Length of output: 46


🏁 Script executed:

# Look for WindowHandle definition in slang-rhi
rg "fromWGPUCanvas" external/slang-rhi/ -B 5 -A 15

Repository: shader-slang/slangpy

Length of output: 46


🏁 Script executed:

# Also look for WindowHandle class in slang-rhi
rg "class WindowHandle|struct WindowHandle" external/slang-rhi/ -B 3 -A 20

Repository: shader-slang/slangpy

Length of output: 46


🏁 Script executed:

# Check if there are any comments or documentation about fromWGPUCanvas in the codebase
rg "WGPUCanvas|canvas.*selector" -i --type cpp --type h | head -30

Repository: shader-slang/slangpy

Length of output: 326


🏁 Script executed:

# Look for any error handling patterns in surface.cpp
cat -n src/sgl/device/surface.cpp

Repository: shader-slang/slangpy

Length of output: 4023


🏁 Script executed:

# Search for how null validation is done elsewhere in similar code
rg "SGL_THROW\|SGL_ASSERT" src/sgl/device/ -B 2 -A 2 | head -60

Repository: shader-slang/slangpy

Length of output: 46


Add null validation for Emscripten canvas selector.

The public Surface(WindowHandle, ref<Device>) constructor can receive a default-initialized WindowHandle{} where canvasSelector == nullptr, which is then passed unchecked to rhi::WindowHandle::fromWGPUCanvas(). While the Window* constructor path supplies "#canvas" safely, direct instantiation bypasses this protection.

Suggested guard
 `#elif` SGL_EMSCRIPTEN
+    if (!window_handle.canvasSelector)
+        SGL_THROW("Emscripten surface creation requires a non-null canvas selector.");
     rhi::WindowHandle rhi_window_handle = rhi::WindowHandle::fromWGPUCanvas(window_handle.canvasSelector);
 `#endif`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#elif SGL_EMSCRIPTEN
rhi::WindowHandle rhi_window_handle = rhi::WindowHandle::fromWGPUCanvas(window_handle.canvasSelector);
`#elif` SGL_EMSCRIPTEN
if (!window_handle.canvasSelector)
SGL_THROW("Emscripten surface creation requires a non-null canvas selector.");
rhi::WindowHandle rhi_window_handle = rhi::WindowHandle::fromWGPUCanvas(window_handle.canvasSelector);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/device/surface.cpp` around lines 26 - 27, The Surface(WindowHandle,
ref<Device>) constructor passes window_handle.canvasSelector directly into
rhi::WindowHandle::fromWGPUCanvas without checking for null; add a guard in the
Surface constructor to detect a null or empty canvasSelector on the incoming
WindowHandle and substitute a safe default (e.g. "#canvas") before calling
rhi::WindowHandle::fromWGPUCanvas. Locate the branch for SGL_EMSCRIPTEN in
surface.cpp where
rhi::WindowHandle::fromWGPUCanvas(window_handle.canvasSelector) is used and wrap
it so that if window_handle.canvasSelector is null/empty you call fromWGPUCanvas
with the default selector instead.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/sgl/device/device.cpp (3)

1346-1358: ⚠️ Potential issue | 🟡 Minor

Fail fast when CUDA context handles are requested without CUDA support.

Returning default/invalid handles when SGL_HAS_CUDA is false can propagate hard-to-debug failures to callers. Prefer throwing an explicit error instead.

Suggested fix
 std::array<NativeHandle, 3> get_cuda_current_context_native_handles()
 {
     std::array<NativeHandle, 3> handles;
 `#if` SGL_HAS_CUDA
     CUcontext cu_context;
     SGL_CHECK(rhiCudaDriverApiInit(), "Failed to initialize CUDA driver API.");
     SGL_CU_CHECK(cuCtxGetCurrent(&cu_context));
     SGL_CHECK(cu_context, "No current CUDA context found.");
@@
     handles[0] = NativeHandle(cu_device);
     handles[1] = NativeHandle(cu_context);
+#else
+    SGL_THROW("CUDA is not available in this build.");
 `#endif`
     return handles;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/device/device.cpp` around lines 1346 - 1358, The code returns
default/invalid NativeHandle values when SGL_HAS_CUDA is false; change this to
fail fast by adding an `#else` branch that throws an explicit error (e.g., throw
std::runtime_error("CUDA support not compiled in; cannot return CUDA handles")
or use the project’s SGL_CHECK/SGL_ERROR macro) instead of returning handles,
referencing the handled symbols NativeHandle and the handles array (and existing
SGL_CHECK/SGL_CU_CHECK usage) so callers get a clear immediate failure when CUDA
context/device handles are requested without CUDA support.

869-905: ⚠️ Potential issue | 🟠 Major

Do not silently ignore cuda_stream in non-CUDA builds.

With SGL_HAS_CUDA disabled, a valid cuda_stream can currently be accepted and then dropped by forcing .cudaStream = nullptr. This should be rejected to avoid silent misconfiguration.

Suggested fix
     SGL_CHECK(
         !cuda_stream.is_valid() || cuda_stream.type() == NativeHandleType::CUstream,
         "Native handle supplied for CUDA stream is not of type CUstream."
     );
+
+#if !SGL_HAS_CUDA
+    SGL_CHECK(!cuda_stream.is_valid(), "CUDA stream is not supported in builds without CUDA support.");
+#endif

Also applies to: 945-949

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/device/device.cpp` around lines 869 - 905, When SGL_HAS_CUDA is
disabled the code currently drops a valid cuda_stream (silently accepting and
setting cuda_stream_ptr=null), so add an explicit rejection: where the code
branches based on SGL_HAS_CUDA and m_supports_cuda_interop, check
cuda_stream.is_valid() in the non-CUDA build/path and fail fast with SGL_CHECK
(or similar) instead of silently ignoring it. Concretely, update the logic
around cuda_stream/cuda_stream_ptr (symbols: cuda_stream, cuda_stream_ptr,
m_desc.type, DeviceType::cuda, m_supports_cuda_interop, needs_cuda_sync,
sync_to_cuda, copy_from_cuda) so that when SGL_HAS_CUDA is not defined and CUDA
interop is not actually available any valid cuda_stream triggers SGL_CHECK("CUDA
stream is not supported on this device.") (also apply the same guard at the
other occurrence referenced in the comment).

133-205: ⚠️ Potential issue | 🟠 Major

Handle enable_cuda_interop explicitly when CUDA is not compiled in.

When SGL_HAS_CUDA is false, this whole init path is removed, but m_desc.enable_cuda_interop can remain true and still affect behavior later (for example shared fence creation at Line 364). Please fail fast or disable the flag in non-CUDA builds.

Suggested fix
+ `#if` !SGL_HAS_CUDA
+    if (m_desc.enable_cuda_interop) {
+        m_desc.enable_cuda_interop = false;
+        log_warn("CUDA interop requested, but this build has no CUDA support. Disabling enable_cuda_interop.");
+    }
+ `#endif`
+
 `#if` SGL_HAS_CUDA
     // If CUDA interop is enabled on non-cuda backend, check if existing CUDA context or device
     // is provided. If so, we will attempt to identify the same device for use with SlangPy.
     if (m_desc.enable_cuda_interop) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/device/device.cpp` around lines 133 - 205, When SGL_HAS_CUDA is not
defined, explicitly handle m_desc.enable_cuda_interop to avoid later surprises:
at the start of the CUDA-init branch (or earlier in Device initialization)
detect if m_desc.enable_cuda_interop is true and either call close() followed by
SGL_THROW with a clear message that CUDA interop was requested but the build
lacks CUDA support, or set m_desc.enable_cuda_interop = false and emit a
log_warn explaining CUDA interop is disabled in this build; update the code
around the existing SGL_HAS_CUDA / CUDA initialization block that references
m_desc.enable_cuda_interop to implement this fail-fast or safe-noop behavior.
♻️ Duplicate comments (1)
src/sgl/core/window.cpp (1)

420-422: ⚠️ Potential issue | 🟡 Minor

Avoid hardcoding the canvas selector.

This still couples every WASM window to a fixed #canvas DOM id. If the host page uses a different element or needs more than one canvas, surface creation will fail. This is the same issue raised in the previous review.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sgl/core/window.cpp` around lines 420 - 422, The code currently hardcodes
handle.canvasSelector = "#canvas" under SGL_EMSCRIPTEN; change this to use a
configurable value instead: expose a parameter or config field (e.g., a
constructor/initializer argument or WindowOptions) that supplies the canvas
selector and fall back to "#canvas" only as the default, and update the code
that initializes the window handle (the place assigning handle.canvasSelector
and any callers of the window creation/initialization functions) to read the
provided selector; keep the SGL_EMSCRIPTEN conditional but use the configurable
value rather than a fixed string so hosts can pass different selectors or
multiple canvases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/sgl/device/device.cpp`:
- Around line 1346-1358: The code returns default/invalid NativeHandle values
when SGL_HAS_CUDA is false; change this to fail fast by adding an `#else` branch
that throws an explicit error (e.g., throw std::runtime_error("CUDA support not
compiled in; cannot return CUDA handles") or use the project’s
SGL_CHECK/SGL_ERROR macro) instead of returning handles, referencing the handled
symbols NativeHandle and the handles array (and existing SGL_CHECK/SGL_CU_CHECK
usage) so callers get a clear immediate failure when CUDA context/device handles
are requested without CUDA support.
- Around line 869-905: When SGL_HAS_CUDA is disabled the code currently drops a
valid cuda_stream (silently accepting and setting cuda_stream_ptr=null), so add
an explicit rejection: where the code branches based on SGL_HAS_CUDA and
m_supports_cuda_interop, check cuda_stream.is_valid() in the non-CUDA build/path
and fail fast with SGL_CHECK (or similar) instead of silently ignoring it.
Concretely, update the logic around cuda_stream/cuda_stream_ptr (symbols:
cuda_stream, cuda_stream_ptr, m_desc.type, DeviceType::cuda,
m_supports_cuda_interop, needs_cuda_sync, sync_to_cuda, copy_from_cuda) so that
when SGL_HAS_CUDA is not defined and CUDA interop is not actually available any
valid cuda_stream triggers SGL_CHECK("CUDA stream is not supported on this
device.") (also apply the same guard at the other occurrence referenced in the
comment).
- Around line 133-205: When SGL_HAS_CUDA is not defined, explicitly handle
m_desc.enable_cuda_interop to avoid later surprises: at the start of the
CUDA-init branch (or earlier in Device initialization) detect if
m_desc.enable_cuda_interop is true and either call close() followed by SGL_THROW
with a clear message that CUDA interop was requested but the build lacks CUDA
support, or set m_desc.enable_cuda_interop = false and emit a log_warn
explaining CUDA interop is disabled in this build; update the code around the
existing SGL_HAS_CUDA / CUDA initialization block that references
m_desc.enable_cuda_interop to implement this fail-fast or safe-noop behavior.

---

Duplicate comments:
In `@src/sgl/core/window.cpp`:
- Around line 420-422: The code currently hardcodes handle.canvasSelector =
"#canvas" under SGL_EMSCRIPTEN; change this to use a configurable value instead:
expose a parameter or config field (e.g., a constructor/initializer argument or
WindowOptions) that supplies the canvas selector and fall back to "#canvas" only
as the default, and update the code that initializes the window handle (the
place assigning handle.canvasSelector and any callers of the window
creation/initialization functions) to read the provided selector; keep the
SGL_EMSCRIPTEN conditional but use the configurable value rather than a fixed
string so hosts can pass different selectors or multiple canvases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02cbe91b-7381-48a7-b9d2-5f5be8b10999

📥 Commits

Reviewing files that changed from the base of the PR and between eb0fbea and 44b52da.

📒 Files selected for processing (3)
  • src/sgl/core/window.cpp
  • src/sgl/device/device.cpp
  • src/sgl/device/device.h

@ccummingsNV

Copy link
Copy Markdown
Contributor

Worth reading through some of code rabbit's points as some seem valid - could you either address them or mark resolved.

For thread.h, the last time I looked at WASM threads were still a proposal. If that's not changed I guess this needs to be pretty limited to raising errors where separate threads are attempted to be spawned.

@j8asic

j8asic commented Apr 30, 2026

Copy link
Copy Markdown
Author

Sure. The comments are mostly okay, I need to find time to address them.

@j8asic
j8asic marked this pull request as draft May 5, 2026 03:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants