Skip to content

Add Embree 4.4.1 C# bindings - #1

Merged
jcant0n merged 5 commits into
mainfrom
feature/embree-bindings
Aug 11, 2026
Merged

Add Embree 4.4.1 C# bindings#1
jcant0n merged 5 commits into
mainfrom
feature/embree-bindings

Conversation

@jcant0n

@jcant0n jcant0n commented Aug 11, 2026

Copy link
Copy Markdown
Member

Low-level P/Invoke bindings for Intel Embree 4.4.1,
following the Evergine binding pattern: a CppAst-based generator parses the vendored Embree
headers and emits the .cs files that make up Evergine.Bindings.Embree.

What is generated

153 functions, 13 enums, 35 structs, 15 function-pointer typedefs and 7 opaque handle types,
split into Constants.cs, Enums.cs, Delegates.cs, Handles.cs, Structs.cs and
Functions.cs. The full C API is covered: devices, scenes, all geometry types, ray queries
(1/4/8/16 plus the Traversable and Forward variants), point queries, the BVH builder and
every callback.

Points worth reviewing

  • Deterministic parsing. The generator runs in C mode against stub system headers in
    EmbreeGen/Headers/stubs and a pinned x86_64-pc-windows target, so the output is identical
    on a Windows dev box and on the linux-x64 CI runner. Without the stubs it picked up whatever
    MSVC or libc headers the machine happened to have.
  • rtcore_config.h is vendored. CMake generates it and it decides the layout of several
    public structs (RTC_MAX_INSTANCE_LEVEL_COUNT, RTC_GEOMETRY_INSTANCE_ARRAY,
    RTC_MIN_WIDTH). The native build workflow uploads it next to the binaries so the two cannot
    drift; HelloEmbree asserts the resulting struct sizes at startup.
  • Explicit StructLayout.Size. Embree declares most structs RTC_ALIGN(16/32/64), which
    rounds their C sizeof up: RTCHit has 36 bytes of fields but is 48 bytes wide. A plain
    sequential C# struct would be narrower and RTCRayHit would be laid out wrong, so generated
    structs carry the size clang computed.
  • RTC_FORCEINLINE helpers. They have no exported symbol, so the generator skips them and
    hand-written equivalents live in Embree.Inline.cs, together with the SoA RayN_*/HitN_*
    packet accessors from the C++ section of rtcore_ray.h.
  • C bool maps to byte, not bool: the default .NET marshalling of bool is the 4-byte
    Win32 BOOL, not the 1-byte C _Bool.
  • A module initializer registers a DllImport resolver so project references behave like the
    NuGet package.

Samples

  • HelloEmbree — console smoke test: asserts struct sizes, then runs rtcIntersect1 and
    rtcOccluded1 against a triangle.
  • HelloEmbree.Evergine — CPU ray tracer drawn through the Evergine low-level graphics API,
    hosted in a Windows Forms window, with a --bench mode reporting per-stage cost.

The Evergine sample running

One thing this surfaced that is worth knowing when using the binding: RTCRay and RTCRayHit
must be 16-byte aligned
. Embree's kernels use aligned SIMD loads and a C# local carries no such
guarantee, so &someLocal crashes on some code paths and silently works on others. Both samples
allocate rays with NativeMemory.AlignedAlloc, and the READMEs call it out.

Not done yet

  • No native binaries. runtimes/ only has .gitkeep. They are produced by the manually
    dispatched Build Embree Libraries workflow (EMBREE_TASKING_SYSTEM=INTERNAL, so each binary
    is self-contained with no TBB dependency) and have to be committed together with the
    rtcore_config.h from the same build. Until then neither sample runs from a fresh clone.
  • win-arm64 is untested. Its windows-11-arm runner leg may need toolchain adjustments; if
    it does not work, dropping that RID from the first release and from the README is reasonable.
  • Verified locally against the official Intel embree4.dll for win-x64, whose rtcore_config.h
    matches the vendored one exactly.

Low-level P/Invoke bindings for Intel Embree following the Evergine binding
pattern: a CppAst-based generator parses the vendored Embree 4.4.1 headers and
emits Constants/Enums/Delegates/Handles/Structs/Functions into
Evergine.Bindings.Embree (153 functions, 13 enums, 35 structs, 15 function
pointer typedefs, 7 opaque handles).

Notable points:

- The generator parses in C mode against stub system headers under
  EmbreeGen/Headers/stubs and a pinned x86_64 target, so the output is identical
  on a Windows dev box and on the linux-x64 CI runner.
- rtcore_config.h is vendored because CMake generates it and it decides the
  layout of several public structs. The native build workflow uploads it
  alongside the binaries so the two never drift.
- Embree declares most structs RTC_ALIGN(16/32/64), which rounds their C sizeof
  up (RTCHit is 36 bytes of fields but 48 bytes wide). Generated structs carry
  an explicit StructLayout Size so RTCRayHit and friends match the C layout.
- The RTC_FORCEINLINE helpers have no exported symbol, so hand-written
  equivalents live in Embree.Inline.cs together with the SoA RayN_/HitN_
  packet accessors.
- A module initializer registers a DllImport resolver so project references
  behave like the NuGet package.

Samples:

- HelloEmbree: console smoke test that asserts struct sizes and runs
  rtcIntersect1/rtcOccluded1 against a triangle.
- HelloEmbree.Evergine: CPU ray tracer drawn through the Evergine low-level
  graphics API, with a --bench mode reporting per-stage cost.

Native binaries are produced by the manually dispatched Build Embree Libraries
workflow (EMBREE_TASKING_SYSTEM=INTERNAL, five RIDs) and are not committed yet.
HelloEmbreeEvergine was only chosen because HelloEmbree.Evergine shadowed the
Evergine.* namespaces from inside the project. Plain HelloEmbree has no such
clash and matches the console sample.
The sample used to let FormsWindowsSystem create its own window. It now owns a
MainForm with a toolbar and a status bar, and Evergine renders into an
EvergineControl docked inside it, so the ray traced image sits in a normal
WinForms layout.

The HWND is read after the control is parented, because WinForms recreates a
control's handle when it is added to a container. AutoRegisterWindow is turned
off and the render loop is pointed at the form, so closing the window ends it.

The status bar shows the live per-stage timings, the toolbar can freeze the
camera and save a PNG on demand, and resizing only resizes the swapchain: the
ray traced image keeps its own resolution and is stretched by the fullscreen
triangle, so it costs nothing on the CPU.
…tput

CI failed with 'script not found at: build/scripts/Generate-Bindings-DotNet.ps1'.
The reusable binding-common-ci workflow calls that script, which is one of the
files evergine-standards syncs into every binding repo; it was missing here.
Copied the three scripts from the sibling binding repos, sync-standards will
keep them up to date from now on.

That surfaced a latent bug. The script runs the generator from
bin/<cfg>/<tfm>/<rid>/publish/, one level deeper than a local dotnet run, so
the hardcoded '../../../../..' resolved to EmbreeGen/ instead of the repository
root and the bindings would have been written to the wrong place without any
error. The generator now walks up looking for the binding project and fails
loudly if it cannot find it.
Removes the console smoke test and renames HelloEmbree.Evergine to HelloEmbree,
taking over the freed name: project file, solution entry, README and gitignore
entry follow. The namespace was already HelloEmbree.

The struct-size assertions the console sample existed for are also in the
Evergine sample's CheckStructLayouts, so the rtcore_config.h drift check
survives the removal. What is lost is the only sample that ran outside Windows:
what remains is WinForms plus DX11.
@jcant0n
jcant0n merged commit a6bb2b5 into main Aug 11, 2026
1 check passed
@jcant0n
jcant0n deleted the feature/embree-bindings branch August 11, 2026 17:10
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.

1 participant