From 5f96fb5c8c1459b76fdebc0c6bf3b400cfaeedc0 Mon Sep 17 00:00:00 2001 From: shivaduke28 Date: Mon, 16 Mar 2026 01:27:23 +0900 Subject: [PATCH 1/2] Expand tests to cover reflection, user attributes, and default values Add 11 new tests covering: - TypeReflection: scalar, vector, matrix types - Struct reflection: field count, field types, field names - TypeLayout: stride, alignment, field offsets - User attributes: top-level and struct field attributes - Default values: float and int struct field defaults - Resource types: Texture2D and SamplerState parameters - Entry points: compute thread group size, multiple entry points Closes #16 Co-Authored-By: Claude Opus 4.6 (1M context) --- Tests/SwiftSlangTests/SwiftSlangTests.swift | 394 +++++++++++++++++++- 1 file changed, 375 insertions(+), 19 deletions(-) diff --git a/Tests/SwiftSlangTests/SwiftSlangTests.swift b/Tests/SwiftSlangTests/SwiftSlangTests.swift index 00ba036..c4b341a 100644 --- a/Tests/SwiftSlangTests/SwiftSlangTests.swift +++ b/Tests/SwiftSlangTests/SwiftSlangTests.swift @@ -3,6 +3,8 @@ import SwiftSlang final class SwiftSlangTests: XCTestCase { + // MARK: - Basic + func testGlobalSessionCreation() throws { let globalSession = try SLGlobalSession.create() XCTAssertNotNil(globalSession) @@ -12,41 +14,395 @@ final class SwiftSlangTests: XCTestCase { func testBasicCompile() throws { let globalSession = try SLGlobalSession.create() - let profile = globalSession.findProfile("sm_5_0") - let targetDesc = SLTargetDesc(format: .metal, profile: profile) let sessionDesc = SLSessionDesc() sessionDesc.targets = [targetDesc] - let session = try globalSession.createSession(with: sessionDesc) - let shaderSource = """ + let source = """ [shader("vertex")] float4 vertexMain(float3 position : POSITION) : SV_Position { return float4(position, 1.0); } """ + let module = try session.loadModule(fromSourceString: "TestShader", path: "", source: source) + let entryPoint = try module.findEntryPoint(byName: "vertexMain") + let composite = try session.createCompositeComponentType(with: module, entryPoints: [entryPoint]) + let linked = try composite.link() + let metalCode = try linked.getTargetCode(0) + XCTAssertGreaterThan(metalCode.count, 0) + } - let module = try session.loadModule( - fromSourceString: "TestShader", - path: "", - source: shaderSource - ) - XCTAssertEqual(module.name, "TestShader") + // MARK: - TypeReflection - let entryPoint = try module.findEntryPoint(byName: "vertexMain") - XCTAssertEqual(entryPoint.name, "vertexMain") - XCTAssertEqual(entryPoint.stage, .vertex) + func testScalarTypeReflection() throws { + let globalSession = try SLGlobalSession.create() + let profile = globalSession.findProfile("sm_5_0") + let targetDesc = SLTargetDesc(format: .metal, profile: profile) + let sessionDesc = SLSessionDesc() + sessionDesc.targets = [targetDesc] + let session = try globalSession.createSession(with: sessionDesc) + + let source = """ + uniform float scalarParam; + [shader("fragment")] + float4 fragMain() : SV_Target { return float4(scalarParam, 0, 0, 1); } + """ + let module = try session.loadModule(fromSourceString: "Test", path: "", source: source) + let entryPoint = try module.entryPoint(at: 0) + let composite = try session.createCompositeComponentType(with: module, entryPoints: [entryPoint]) + let linked = try composite.link() + let params = try linked.getShaderParameters(0) + + let uniformParam = params.first { $0.name == "scalarParam" } + XCTAssertNotNil(uniformParam) + let typeLayout = try XCTUnwrap(uniformParam?.typeLayout) + let type = try XCTUnwrap(typeLayout.getType()) + XCTAssertEqual(type.getKind(), .scalar) + XCTAssertEqual(type.getScalarType(), .float32) + } - let composite = try session.createCompositeComponentType( - with: module, - entryPoints: [entryPoint] - ) + func testVectorTypeReflection() throws { + let globalSession = try SLGlobalSession.create() + let profile = globalSession.findProfile("sm_5_0") + let targetDesc = SLTargetDesc(format: .metal, profile: profile) + let sessionDesc = SLSessionDesc() + sessionDesc.targets = [targetDesc] + let session = try globalSession.createSession(with: sessionDesc) + let source = """ + uniform float3 colorParam; + [shader("fragment")] + float4 fragMain() : SV_Target { return float4(colorParam, 1); } + """ + let module = try session.loadModule(fromSourceString: "Test", path: "", source: source) + let entryPoint = try module.entryPoint(at: 0) + let composite = try session.createCompositeComponentType(with: module, entryPoints: [entryPoint]) let linked = try composite.link() + let params = try linked.getShaderParameters(0) - let metalCode = try linked.getTargetCode(0) - XCTAssertGreaterThan(metalCode.count, 0) + let uniformParam = params.first { $0.name == "colorParam" } + let typeLayout = try XCTUnwrap(uniformParam?.typeLayout) + let type = try XCTUnwrap(typeLayout.getType()) + XCTAssertEqual(type.getKind(), .vector) + XCTAssertEqual(type.getScalarType(), .float32) + XCTAssertEqual(type.getElementCount(), 3) + } + + func testMatrixTypeReflection() throws { + let globalSession = try SLGlobalSession.create() + let profile = globalSession.findProfile("sm_5_0") + let targetDesc = SLTargetDesc(format: .metal, profile: profile) + let sessionDesc = SLSessionDesc() + sessionDesc.targets = [targetDesc] + let session = try globalSession.createSession(with: sessionDesc) + + let source = """ + uniform float4x4 matParam; + [shader("vertex")] + float4 vertMain(float3 pos : POSITION) : SV_Position { return mul(matParam, float4(pos, 1)); } + """ + let module = try session.loadModule(fromSourceString: "Test", path: "", source: source) + let entryPoint = try module.entryPoint(at: 0) + let composite = try session.createCompositeComponentType(with: module, entryPoints: [entryPoint]) + let linked = try composite.link() + let params = try linked.getShaderParameters(0) + + let uniformParam = params.first { $0.name == "matParam" } + let typeLayout = try XCTUnwrap(uniformParam?.typeLayout) + let type = try XCTUnwrap(typeLayout.getType()) + XCTAssertEqual(type.getKind(), .matrix) + XCTAssertEqual(type.getRowCount(), 4) + XCTAssertEqual(type.getColumnCount(), 4) + XCTAssertEqual(type.getScalarType(), .float32) + } + + // MARK: - Struct & TypeLayout + + func testStructReflection() throws { + let globalSession = try SLGlobalSession.create() + let profile = globalSession.findProfile("sm_5_0") + let targetDesc = SLTargetDesc(format: .metal, profile: profile) + let sessionDesc = SLSessionDesc() + sessionDesc.targets = [targetDesc] + let session = try globalSession.createSession(with: sessionDesc) + + let source = """ + struct MyParams { float intensity; float3 color; int mode; }; + uniform MyParams params; + [shader("fragment")] + float4 fragMain() : SV_Target { return float4(params.color * params.intensity, 1); } + """ + let module = try session.loadModule(fromSourceString: "Test", path: "", source: source) + let entryPoint = try module.entryPoint(at: 0) + let composite = try session.createCompositeComponentType(with: module, entryPoints: [entryPoint]) + let linked = try composite.link() + let shaderParams = try linked.getShaderParameters(0) + + let param = shaderParams.first { $0.name == "params" || $0.name == "data" } + XCTAssertNotNil(param) + + let typeLayout = try XCTUnwrap(param?.typeLayout) + let structType = try XCTUnwrap(typeLayout.getType()) + XCTAssertEqual(structType.getKind(), .struct) + XCTAssertEqual(structType.getName(), "MyParams") + XCTAssertEqual(structType.getFieldCount(), 3) + + let field0 = try XCTUnwrap(structType.getFieldBy(0)) + XCTAssertEqual(field0.getName(), "intensity") + let field0Type = try XCTUnwrap(field0.getType()) + XCTAssertEqual(field0Type.getKind(), .scalar) + XCTAssertEqual(field0Type.getScalarType(), .float32) + + let field1 = try XCTUnwrap(structType.getFieldBy(1)) + XCTAssertEqual(field1.getName(), "color") + let field1Type = try XCTUnwrap(field1.getType()) + XCTAssertEqual(field1Type.getKind(), .vector) + XCTAssertEqual(field1Type.getElementCount(), 3) + + let field2 = try XCTUnwrap(structType.getFieldBy(2)) + XCTAssertEqual(field2.getName(), "mode") + let field2Type = try XCTUnwrap(field2.getType()) + XCTAssertEqual(field2Type.getScalarType(), .int32) + } + + func testTypeLayoutStrideAndAlignment() throws { + let globalSession = try SLGlobalSession.create() + let profile = globalSession.findProfile("sm_5_0") + let targetDesc = SLTargetDesc(format: .metal, profile: profile) + let sessionDesc = SLSessionDesc() + sessionDesc.targets = [targetDesc] + let session = try globalSession.createSession(with: sessionDesc) + + let source = """ + struct MyData { float a; float b; }; + uniform MyData data; + [shader("fragment")] + float4 fragMain() : SV_Target { return float4(data.a, data.b, 0, 1); } + """ + let module = try session.loadModule(fromSourceString: "Test", path: "", source: source) + let entryPoint = try module.entryPoint(at: 0) + let composite = try session.createCompositeComponentType(with: module, entryPoints: [entryPoint]) + let linked = try composite.link() + let shaderParams = try linked.getShaderParameters(0) + + let param = shaderParams.first { $0.name == "params" || $0.name == "data" } + let typeLayout = try XCTUnwrap(param?.typeLayout) + + XCTAssertEqual(typeLayout.getFieldCount(), 2) + XCTAssertEqual(typeLayout.size, 8) + XCTAssertEqual(typeLayout.getStride(.uniform), 8) + XCTAssertGreaterThan(typeLayout.getAlignment(.uniform), 0) + + let field0Layout = try XCTUnwrap(typeLayout.getFieldBy(0)) + XCTAssertEqual(field0Layout.getName(), "a") + XCTAssertEqual(field0Layout.getOffset(.uniform), 0) + + let field1Layout = try XCTUnwrap(typeLayout.getFieldBy(1)) + XCTAssertEqual(field1Layout.getName(), "b") + XCTAssertEqual(field1Layout.getOffset(.uniform), 4) + } + + // MARK: - User Attributes + + func testUserAttributes() throws { + let globalSession = try SLGlobalSession.create() + let profile = globalSession.findProfile("sm_5_0") + let targetDesc = SLTargetDesc(format: .metal, profile: profile) + let sessionDesc = SLSessionDesc() + sessionDesc.targets = [targetDesc] + let session = try globalSession.createSession(with: sessionDesc) + + let source = """ + [__AttributeUsage(_AttributeTargets.Var)] + struct rangeAttribute { float minVal; float defaultVal; float maxVal; }; + + [range(0.0, 0.5, 1.0)] + uniform float intensity; + [shader("fragment")] + float4 fragMain() : SV_Target { return float4(intensity, 0, 0, 1); } + """ + let module = try session.loadModule(fromSourceString: "Test", path: "", source: source) + let entryPoint = try module.entryPoint(at: 0) + let composite = try session.createCompositeComponentType(with: module, entryPoints: [entryPoint]) + let linked = try composite.link() + let params = try linked.getShaderParameters(0) + + let uniformParam = params.first { $0.name == "intensity" } + XCTAssertNotNil(uniformParam) + XCTAssertGreaterThan(uniformParam!.userAttributes.count, 0) + + let rangeAttr = uniformParam!.userAttributes.first { $0.name == "range" } + XCTAssertNotNil(rangeAttr) + XCTAssertEqual(rangeAttr!.argumentCount, 3) + XCTAssertEqual(rangeAttr!.floatArguments[0].floatValue, 0.0, accuracy: 0.001) + XCTAssertEqual(rangeAttr!.floatArguments[1].floatValue, 0.5, accuracy: 0.001) + XCTAssertEqual(rangeAttr!.floatArguments[2].floatValue, 1.0, accuracy: 0.001) + } + + func testUserAttributeViaVariableReflection() throws { + let globalSession = try SLGlobalSession.create() + let profile = globalSession.findProfile("sm_5_0") + let targetDesc = SLTargetDesc(format: .metal, profile: profile) + let sessionDesc = SLSessionDesc() + sessionDesc.targets = [targetDesc] + let session = try globalSession.createSession(with: sessionDesc) + + let source = """ + [__AttributeUsage(_AttributeTargets.Var)] + struct colorAttribute { int enabled; }; + + struct MyParams { [color(1)] float3 tint; }; + uniform MyParams params; + [shader("fragment")] + float4 fragMain() : SV_Target { return float4(params.tint, 1); } + """ + let module = try session.loadModule(fromSourceString: "Test", path: "", source: source) + let entryPoint = try module.entryPoint(at: 0) + let composite = try session.createCompositeComponentType(with: module, entryPoints: [entryPoint]) + let linked = try composite.link() + let shaderParams = try linked.getShaderParameters(0) + + let param = shaderParams.first { $0.name == "params" || $0.name == "data" } + let typeLayout = try XCTUnwrap(param?.typeLayout) + + // Access field via TypeLayout → VariableLayoutReflection → VariableReflection + XCTAssertEqual(typeLayout.getFieldCount(), 1) + let tintFieldLayout = try XCTUnwrap(typeLayout.getFieldBy(0)) + let tintVariable = try XCTUnwrap(tintFieldLayout.getVariable()) + XCTAssertEqual(tintVariable.getName(), "tint") + XCTAssertEqual(tintVariable.getUserAttributeCount(), 1) + + let colorAttr = try XCTUnwrap(tintVariable.getUserAttribute(by: 0)) + XCTAssertEqual(colorAttr.name, "color") + XCTAssertEqual(colorAttr.argumentCount, 1) + XCTAssertEqual(colorAttr.intArguments[0].intValue, 1) + } + + // MARK: - Default Values + + func testDefaultValues() throws { + let globalSession = try SLGlobalSession.create() + let profile = globalSession.findProfile("sm_5_0") + let targetDesc = SLTargetDesc(format: .metal, profile: profile) + let sessionDesc = SLSessionDesc() + sessionDesc.targets = [targetDesc] + let session = try globalSession.createSession(with: sessionDesc) + + let source = """ + struct Params { float brightness = 0.75; int count = 3; }; + uniform Params params; + [shader("fragment")] + float4 fragMain() : SV_Target { return float4(params.brightness, 0, 0, float(params.count)); } + """ + let module = try session.loadModule(fromSourceString: "Test", path: "", source: source) + let entryPoint = try module.entryPoint(at: 0) + let composite = try session.createCompositeComponentType(with: module, entryPoints: [entryPoint]) + let linked = try composite.link() + let shaderParams = try linked.getShaderParameters(0) + + let param = shaderParams.first { $0.name == "params" || $0.name == "data" } + let typeLayout = try XCTUnwrap(param?.typeLayout) + let structType = try XCTUnwrap(typeLayout.getType()) + + let brightnessField = try XCTUnwrap(structType.getFieldBy(0)) + XCTAssertEqual(brightnessField.getName(), "brightness") + XCTAssertTrue(brightnessField.hasDefaultValue()) + let floatVal = try XCTUnwrap(brightnessField.getDefaultValueFloat()) + XCTAssertEqual(floatVal.floatValue, 0.75, accuracy: 0.001) + + let countField = try XCTUnwrap(structType.getFieldBy(1)) + XCTAssertEqual(countField.getName(), "count") + XCTAssertTrue(countField.hasDefaultValue()) + let intVal = try XCTUnwrap(countField.getDefaultValueInt()) + XCTAssertEqual(intVal.int64Value, 3) + } + + // MARK: - Resource Types + + func testTextureAndSamplerParameters() throws { + let globalSession = try SLGlobalSession.create() + let profile = globalSession.findProfile("sm_5_0") + let targetDesc = SLTargetDesc(format: .metal, profile: profile) + let sessionDesc = SLSessionDesc() + sessionDesc.targets = [targetDesc] + let session = try globalSession.createSession(with: sessionDesc) + + let source = """ + Texture2D myTexture; + SamplerState mySampler; + [shader("fragment")] + float4 fragMain(float2 uv : TEXCOORD) : SV_Target { return myTexture.Sample(mySampler, uv); } + """ + let module = try session.loadModule(fromSourceString: "Test", path: "", source: source) + let entryPoint = try module.entryPoint(at: 0) + let composite = try session.createCompositeComponentType(with: module, entryPoints: [entryPoint]) + let linked = try composite.link() + let params = try linked.getShaderParameters(0) + + let texParam = params.first { $0.name == "myTexture" } + XCTAssertNotNil(texParam) + XCTAssertEqual(texParam!.category, .shaderResource) + + let samplerParam = params.first { $0.name == "mySampler" } + XCTAssertNotNil(samplerParam) + XCTAssertEqual(samplerParam!.category, .samplerState) + + let texTypeLayout = try XCTUnwrap(texParam?.typeLayout) + let texType = try XCTUnwrap(texTypeLayout.getType()) + XCTAssertEqual(texType.getKind(), .resource) + XCTAssertEqual(texType.getResourceShape(), .texture2D) + XCTAssertEqual(texType.getResourceAccess(), .read) + } + + // MARK: - Entry Point + + func testComputeEntryPoint() throws { + let globalSession = try SLGlobalSession.create() + let profile = globalSession.findProfile("sm_5_0") + let targetDesc = SLTargetDesc(format: .metal, profile: profile) + let sessionDesc = SLSessionDesc() + sessionDesc.targets = [targetDesc] + let session = try globalSession.createSession(with: sessionDesc) + + let source = """ + [shader("compute")] + [numthreads(8, 4, 1)] + void computeMain(uint3 tid : SV_DispatchThreadID) {} + """ + let module = try session.loadModule(fromSourceString: "Test", path: "", source: source) + let entryPoint = try module.findEntryPoint(byName: "computeMain") + XCTAssertEqual(entryPoint.stage, .compute) + + let threadGroupSize = entryPoint.computeThreadGroupSize + XCTAssertEqual(threadGroupSize.count, 3) + XCTAssertEqual(threadGroupSize[0].intValue, 8) + XCTAssertEqual(threadGroupSize[1].intValue, 4) + XCTAssertEqual(threadGroupSize[2].intValue, 1) + } + + func testMultipleEntryPoints() throws { + let globalSession = try SLGlobalSession.create() + let profile = globalSession.findProfile("sm_5_0") + let targetDesc = SLTargetDesc(format: .metal, profile: profile) + let sessionDesc = SLSessionDesc() + sessionDesc.targets = [targetDesc] + let session = try globalSession.createSession(with: sessionDesc) + + let source = """ + [shader("vertex")] + float4 vertMain(float3 pos : POSITION) : SV_Position { return float4(pos, 1); } + [shader("fragment")] + float4 fragMain() : SV_Target { return float4(1, 0, 0, 1); } + """ + let module = try session.loadModule(fromSourceString: "Test", path: "", source: source) + XCTAssertEqual(module.entryPointCount, 2) + + let vertEntry = try module.findEntryPoint(byName: "vertMain") + XCTAssertEqual(vertEntry.stage, .vertex) + + let fragEntry = try module.findEntryPoint(byName: "fragMain") + XCTAssertEqual(fragEntry.stage, .fragment) } } From d57c99c745af7bfd4ae77857bfca4888727653f1 Mon Sep 17 00:00:00 2001 From: shivaduke28 Date: Mon, 16 Mar 2026 01:28:48 +0900 Subject: [PATCH 2/2] Add CLAUDE.md with development notes Document Metal target specifics, user attribute syntax, and object lifetime requirements discovered during testing. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4dffb6d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,29 @@ +# SwiftSlang Development Notes + +## Build & Test + +```bash +# Build +swift build + +# Test (iOS Simulator required) +xcodebuild test \ + -scheme SwiftSlang-Package \ + -destination 'platform=iOS Simulator,name=iPhone 16' \ + -skipPackagePluginValidation +``` + +## Metal Target Specifics + +- `uniform MyStruct params;` のようなstruct uniformは、Metal ターゲットでは `SLParameterCategory.uniform` カテゴリになる(`constantBuffer` ではない) +- `param.typeLayout` で直接 struct の `SLTypeLayout` が取れる(ConstantBuffer の `elementTypeLayout` 経由ではない) + +## User Attributes + +- Slang の user attribute 宣言には `[__AttributeUsage(_AttributeTargets.Var)]` 構文を使う +- struct 名は小文字始まり(例: `rangeAttribute`)、使用時も小文字(例: `[range(0.0, 0.5, 1.0)]`) + +## Object Lifetime + +- `SLGlobalSession` は `SLSession` より長く生存させる必要がある。`SLGlobalSession` が先に解放されると、セッション系 API がクラッシュする +- テストでは各テストメソッド内で `SLGlobalSession` をローカル変数として保持し、メソッドスコープの最後まで生存させること