Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ directly. Windows, macOS, and Linux, on x86_64 and aarch64. Requires JDK 25+.

| Artifact | Purpose |
|--------------------------------------------------------|---|
| `io.github.refux:slang-java:0.0.7` | The library: compiler API, reflection, Java file systems |
| `io.github.refux:slang-java-natives:0.0.7:<os>-<arch>` | The official Slang binaries, as one classifier per platform: `windows`, `linux`, or `macos` × `x86_64` or `aarch64` |
| `io.github.refux:slang-java:0.0.8` | The library: compiler API, reflection, Java file systems |
| `io.github.refux:slang-java-natives:0.0.8:<os>-<arch>` | The official Slang binaries, as one classifier per platform: `windows`, `linux`, or `macos` × `x86_64` or `aarch64` |

The library version tracks slang-java's own API, not Slang's. To see which Slang release a jar
binds, read its manifest — no resolving, no running:

```bash
unzip -p slang-java-0.0.7.jar META-INF/MANIFEST.MF
unzip -p slang-java-0.0.8.jar META-INF/MANIFEST.MF
```

`Slang-Version` there is a **floor**, not a lockstep: it names the release the bindings were
Expand All @@ -46,8 +46,8 @@ val slangNatives = "${
}-${if (System.getProperty("os.arch") in listOf("aarch64", "arm64")) "aarch64" else "x86_64"}"

dependencies {
implementation("io.github.refux:slang-java:0.0.7")
runtimeOnly("io.github.refux:slang-java-natives:0.0.7:$slangNatives")
implementation("io.github.refux:slang-java:0.0.8")
runtimeOnly("io.github.refux:slang-java-natives:0.0.8:$slangNatives")
}

tasks.withType<JavaExec> {
Expand All @@ -60,10 +60,10 @@ classpath together and the loader picks the host's at runtime (LWJGL-style):

```kotlin
dependencies {
implementation("io.github.refux:slang-java:0.0.7")
runtimeOnly("io.github.refux:slang-java-natives:0.0.7:macos-aarch64")
runtimeOnly("io.github.refux:slang-java-natives:0.0.7:windows-x86_64")
runtimeOnly("io.github.refux:slang-java-natives:0.0.7:linux-x86_64")
implementation("io.github.refux:slang-java:0.0.8")
runtimeOnly("io.github.refux:slang-java-natives:0.0.8:macos-aarch64")
runtimeOnly("io.github.refux:slang-java-natives:0.0.8:windows-x86_64")
runtimeOnly("io.github.refux:slang-java-natives:0.0.8:linux-x86_64")
}
```

Expand Down Expand Up @@ -111,6 +111,11 @@ Session session = global.newSession()
.create();
```

Matrices follow `SessionDesc`'s row-major default, which is **not** what the `slangc` command
line uses — compile the same shader offline with `slangc` and at run time through this library
and every matrix comes out transposed relative to the other build, with nothing to diagnose it.
`matrixLayout(MatrixLayout.COLUMN_MAJOR)` on the session builder matches `slangc`.

Everything is `AutoCloseable` with try-with-resources as the idiom; anything left unclosed is
released by a Cleaner when unreachable, and `-Dio.github.refux.slang.debug=true` traces leaks
to their allocation site and asserts session thread confinement.
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ org.gradle.caching=true
# Single source of truth for the published version (library + natives artifacts).
# Release tags must match: tag v0.0.1 <-> version=0.0.1.
group=io.github.refux
version=0.0.7
version=0.0.8
63 changes: 63 additions & 0 deletions slang/src/main/java/io/github/refux/slang/MatrixLayout.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.github.refux.slang;

import io.github.refux.slang.ffi.gen.SlangMatrixLayoutMode;
import java.util.HashMap;
import java.util.Map;

/**
* How a session lays out matrix types ({@code SlangMatrixLayoutMode}), with {@link #value()} /
* {@link #of(int)} escape hatches to the raw ABI values.
*
* <p><b>Slang's own two defaults disagree, so a project that compiles the same shader both ways
* must say which it wants.</b> {@code SessionDesc} in the C++ header defaults to
* {@link #ROW_MAJOR}, and {@link SessionBuilder} follows the header. The {@code slangc} command
* line defaults to column-major instead. Compile a shader offline with {@code slangc} and again at
* run time through this library, and the two SPIR-V modules carry opposite {@code RowMajor} /
* {@code ColMajor} member decorations for the same source — every matrix is silently transposed
* relative to the other build. Nothing diagnoses it: both modules are valid, both load, and the
* only symptom is wrong geometry.
*
* <p>Note the naming inverts on the way to SPIR-V: {@link #COLUMN_MAJOR} here emits {@code RowMajor}
* member decorations, matching what {@code slangc} produces by default.
*/
public enum MatrixLayout {
/** Slang's mode is unset; the compiler picks. */
UNKNOWN(SlangMatrixLayoutMode.SLANG_MATRIX_LAYOUT_MODE_UNKNOWN),
/** The {@code SessionDesc} header default. Emits {@code ColMajor} in SPIR-V. */
ROW_MAJOR(SlangMatrixLayoutMode.SLANG_MATRIX_LAYOUT_ROW_MAJOR),
/** What the {@code slangc} command line uses by default. Emits {@code RowMajor} in SPIR-V. */
COLUMN_MAJOR(SlangMatrixLayoutMode.SLANG_MATRIX_LAYOUT_COLUMN_MAJOR);

private static final Map<Integer, MatrixLayout> BY_VALUE = new HashMap<>();

static {
for (MatrixLayout layout : values()) {
BY_VALUE.put(layout.value, layout);
}
}

private final int value;

MatrixLayout(int value) {
this.value = value;
}

/**
* Maps a raw {@code SlangMatrixLayoutMode} to this enum.
*
* @param value the raw ABI value.
* @return the matching constant, or {@link #UNKNOWN} for a value this enum does not know.
*/
public static MatrixLayout of(int value) {
return BY_VALUE.getOrDefault(value, UNKNOWN);
}

/**
* This layout's raw {@code SlangMatrixLayoutMode} value.
*
* @return the ABI value.
*/
public int value() {
return value;
}
}
18 changes: 18 additions & 0 deletions slang/src/main/java/io/github/refux/slang/SessionBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public final class SessionBuilder {
private final Map<String, String> defines = new LinkedHashMap<>();
private Consumer<String> onDiagnostics;
private SlangFileSystem fileSystem;
private MatrixLayout matrixLayout;

SessionBuilder(GlobalSession global) {
this.global = global;
Expand Down Expand Up @@ -67,6 +68,20 @@ public SessionBuilder onDiagnostics(Consumer<String> consumer) {
return this;
}

/**
* Sets how this session lays out matrices, overriding the {@code SessionDesc} header default of
* {@link MatrixLayout#ROW_MAJOR}.
*
* <p>Set it to {@link MatrixLayout#COLUMN_MAJOR} to match what the {@code slangc} command line
* produces. A project that compiles some shaders offline with {@code slangc} and others at run
* time through this library and does NOT set this gets silently transposed matrices in whichever
* half it compiled here — see {@link MatrixLayout}.
*/
public SessionBuilder matrixLayout(MatrixLayout layout) {
this.matrixLayout = layout;
return this;
}

/**
* Resolves the session's {@code import}s/{@code #include}s through Java instead of the OS
* file system — see {@link SlangFileSystem} for ready-made map- and directory-backed
Expand Down Expand Up @@ -105,6 +120,9 @@ public Session create() {

MemorySegment sessionDesc = SessionDesc.allocate(arena);
SessionDesc.setTargets(sessionDesc, targetArray, targets.size());
if (matrixLayout != null) {
SessionDesc.setDefaultMatrixLayoutMode(sessionDesc, matrixLayout.value());
}
if (!searchPaths.isEmpty()) {
String[] paths = searchPaths.stream().map(Path::toString).toArray(String[]::new);
SessionDesc.setSearchPaths(sessionDesc, Marshal.utf8PointerArray(arena, paths), paths.length);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ public static void setPreprocessorMacros(MemorySegment desc, MemorySegment macro
io.github.refux.slang.ffi.gen.SessionDesc.setPreprocessorMacroCount(desc, count);
}

/**
* Sets how the session lays out matrices. The header default this descriptor is allocated with
* is {@code SLANG_MATRIX_LAYOUT_ROW_MAJOR}, which is NOT what the {@code slangc} command line
* uses — see {@code MatrixLayout} for what that costs a project that compiles both ways.
*/
public static void setDefaultMatrixLayoutMode(MemorySegment desc, int mode) {
io.github.refux.slang.ffi.gen.SessionDesc.setDefaultMatrixLayoutMode(desc, mode);
}

/**
* Points {@code fileSystem} at an {@code ISlangFileSystem} COM pointer (e.g. a
* {@link JavaFileSystem}'s {@code segment()}); the session add-refs it during creation.
Expand Down
102 changes: 102 additions & 0 deletions slang/src/test/java/io/github/refux/slang/MatrixLayoutTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package io.github.refux.slang;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.junit.jupiter.api.Test;

/**
* Pins the matrix layout a session compiles with, and the fact that it is settable at all.
*
* <p>Why this is worth a test of its own: {@code SessionDesc}'s header default is row-major while
* the {@code slangc} command line defaults to column-major, so a project compiling the same shader
* both ways gets silently transposed matrices in one half. Nothing diagnoses that — both modules
* are valid SPIR-V — so the only thing standing between a consumer and a day of debugging is this
* knob and the decoration these assertions read.
*/
class MatrixLayoutTest {

private static final String SOURCE = """
struct Pc { float4x4 uProj; };
[[vk::push_constant]] ConstantBuffer<Pc> pc;
[shader("vertex")]
float4 vertexMain(float3 p) { return mul(pc.uProj, float4(p, 1.0)); }
""";

private static byte[] compileWith(MatrixLayout layout) {
try (GlobalSession global = Slang.createGlobalSession()) {
SessionBuilder builder = global.newSession().target(CompileTarget.SPIRV, t -> t.profile("spirv_1_6"));
if (layout != null) {
builder = builder.matrixLayout(layout);
}
try (Session session = builder.create()) {
Module module = session.loadModuleFromSource("matrix_layout_probe", SOURCE);
try (ComponentType linked = session.composite(module, module.entryPoint("vertexMain"))
.link()) {
return linked.entryPointCode(0, 0);
}
}
}
}

/** SPIR-V decoration opcodes are little-endian words; find OpMemberDecorate's operand. */
private static boolean hasDecoration(byte[] spirv, int decoration) {
ByteBuffer words = ByteBuffer.wrap(spirv).order(ByteOrder.LITTLE_ENDIAN);
for (int offset = 5 * 4; offset + 4 <= spirv.length; offset += 4) {
int word = words.getInt(offset);
int opcode = word & 0xFFFF;
int wordCount = word >>> 16;
if (opcode == OP_MEMBER_DECORATE && wordCount >= 4 && words.getInt(offset + 3 * 4) == decoration) {
return true;
}
if (wordCount == 0) {
break;
}
offset += (wordCount - 1) * 4;
}
return false;
}

private static final int OP_MEMBER_DECORATE = 72;
private static final int DECORATION_ROW_MAJOR = 4;
private static final int DECORATION_COL_MAJOR = 5;

@Test
void columnMajorEmitsRowMajorDecorationsMatchingSlangc() {
byte[] spirv = compileWith(MatrixLayout.COLUMN_MAJOR);

assertTrue(hasDecoration(spirv, DECORATION_ROW_MAJOR), "COLUMN_MAJOR should emit RowMajor, as slangc does");
}

@Test
void rowMajorEmitsColMajorDecorations() {
byte[] spirv = compileWith(MatrixLayout.ROW_MAJOR);

assertTrue(hasDecoration(spirv, DECORATION_COL_MAJOR), "ROW_MAJOR should emit ColMajor");
}

@Test
void theTwoLayoutsProduceDifferentModules() {
assertNotEquals(
hasDecoration(compileWith(MatrixLayout.ROW_MAJOR), DECORATION_ROW_MAJOR),
hasDecoration(compileWith(MatrixLayout.COLUMN_MAJOR), DECORATION_ROW_MAJOR),
"the setting must actually reach the compiler");
}

@Test
void unsetKeepsTheSessionDescHeaderDefault() {
assertTrue(
hasDecoration(compileWith(null), DECORATION_COL_MAJOR),
"an unset layout keeps SessionDesc's row-major header default, which emits ColMajor");
}

@Test
void rawValuesRoundTrip() {
for (MatrixLayout layout : MatrixLayout.values()) {
assertEquals(layout, MatrixLayout.of(layout.value()));
}
}
}