Bind shared lighting ABI and shade authored local lights
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# Add lights to a 3D scene
|
||||
|
||||
Add a **Light** component to a scene entity. The entity's transform places a point
|
||||
or spot light; its rotation aims a spot light along local negative Z. A directional
|
||||
light uses the entity's orientation. Light colors and intensity contribute to the
|
||||
mesh's linear PBR illumination before tone mapping. Sprites and UI retain their
|
||||
unlit tint.
|
||||
|
||||
The version-1 `faset.light` component has three `kind` values:
|
||||
|
||||
| Kind | Position and direction | Useful fields |
|
||||
| --- | --- | --- |
|
||||
| `directional` | Direction from the entity transform | `color`, `intensity`, `casts_shadow` |
|
||||
| `point` | Position from the entity transform; illuminates every direction | `color`, `intensity`, `range` |
|
||||
| `spot` | Position and local negative-Z direction | `color`, `intensity`, `range`, `inner_angle`, `outer_angle` |
|
||||
|
||||
Angles are radians. A spot's inner angle must not exceed its outer angle. Intensity
|
||||
must be nonnegative and range positive. `enabled: false` keeps the component in the
|
||||
scene without contributing light. The `shadow_priority` integer is reserved for the
|
||||
bounded local-shadow scheduler; it does not change brightness.
|
||||
|
||||
In the current rendering checkpoint, one enabled directional light can cast the
|
||||
existing single-map shadow. Point and spot lights illuminate meshes but do not yet
|
||||
cast shadows. The [P3 lighting plan](https://github.com/emil28092005/Faset_Engine/blob/main/docs/superpowers/plans/2026-09-24-p3-lighting.md)
|
||||
tracks cascades and the bounded local-shadow atlas. A scene with no Light component
|
||||
keeps the legacy white sun so older projects retain their appearance. Adding any
|
||||
Light component, even a disabled one, turns off that compatibility fallback. If
|
||||
several directionals are enabled, Faset chooses the one with the smallest stable
|
||||
entity ID and reports a diagnostic for the others.
|
||||
|
||||
## Author a point light through MCP
|
||||
|
||||
Use `faset_schema` to inspect the current field IDs, then send a `faset_scene_edit`
|
||||
batch with the document ID, current revision, and target entity ID. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"document": "REPLACE_WITH_DOCUMENT_ID",
|
||||
"revision": 4,
|
||||
"idempotency_key": "add-red-point-light",
|
||||
"operations": [{
|
||||
"op": "component.add",
|
||||
"entity": "REPLACE_WITH_ENTITY_ID",
|
||||
"type": "faset.light",
|
||||
"fields": {
|
||||
"kind": "point",
|
||||
"color": [1, 0.15, 0.1, 1],
|
||||
"intensity": 8,
|
||||
"range": 6
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
Move the entity with its Transform component. `component.add` fills any omitted
|
||||
light fields from the version-1 schema; use `component.set` for later edits. See
|
||||
[MCP and command line](mcp.md) for revision and retry handling.
|
||||
|
||||
## Supply lights directly from C++
|
||||
|
||||
When building a `faset::render::Snapshot` yourself, set
|
||||
`authored_lights_present` to suppress the compatibility sun in a local-only scene.
|
||||
Provide a stable ID for each light so future shadow scheduling remains independent
|
||||
of submission order.
|
||||
|
||||
```cpp
|
||||
faset::render::Snapshot snapshot;
|
||||
snapshot.authored_lights_present = true;
|
||||
|
||||
faset::render::LocalLight point;
|
||||
point.kind = faset::render::LocalLight::Kind::Point;
|
||||
point.stable_id = "level/torch";
|
||||
point.position = {-2, 1.5f, 0};
|
||||
point.color = {1, 0.3f, 0.1f, 1};
|
||||
point.intensity = 8;
|
||||
point.range = 6;
|
||||
snapshot.local_lights.push_back(point);
|
||||
|
||||
faset::render::LocalLight spot;
|
||||
spot.kind = faset::render::LocalLight::Kind::Spot;
|
||||
spot.stable_id = "level/lamp";
|
||||
spot.position = {2, 3, 0};
|
||||
spot.direction = {0, -1, 0};
|
||||
spot.inner_angle = 0.25f;
|
||||
spot.outer_angle = 0.55f;
|
||||
spot.intensity = 5;
|
||||
spot.range = 9;
|
||||
snapshot.local_lights.push_back(spot);
|
||||
```
|
||||
|
||||
The renderer submits at most 128 local lights per frame in stable-ID order. Later
|
||||
P3 work adds explicit overflow diagnostics and measured light-list optimization.
|
||||
@@ -44,6 +44,7 @@ nav:
|
||||
- Editor workspace: editor/workspace.md
|
||||
- Scene templates: editor/templates.md
|
||||
- Assets and Blender: editor/assets.md
|
||||
- Lighting: editor/lighting.md
|
||||
- GPU visibility and mesh LOD: editor/visibility-lod.md
|
||||
- Build, Play, and export: editor/export.md
|
||||
- Profiling and measurements: editor/profiling.md
|
||||
|
||||
+85
-19
@@ -24,6 +24,32 @@ struct FrameParameters {
|
||||
[[vk::binding(1,0)]] SamplerState shadowSampler;
|
||||
[[vk::binding(2,0)]] Texture2D<float4> colorMap;
|
||||
[[vk::binding(3,0)]] SamplerState colorSampler;
|
||||
// Shared Direct/P2 graphics ABI. The legacy material set remains set 0;
|
||||
// GPU-only instance/visibility records occupy set 2.
|
||||
struct LightingHeader {
|
||||
uint4 counts; // local count, sun enabled, sun shadow enabled, view count
|
||||
float4 sunDirectionIntensity; // xyz world-space ray direction, w intensity
|
||||
float4 sunColor;
|
||||
float4 cameraForwardShadowDistance;
|
||||
float4 cascadeSplits;
|
||||
};
|
||||
struct LocalLightGpu {
|
||||
float4 positionRange;
|
||||
float4 directionCosOuter;
|
||||
float4 colorIntensity;
|
||||
float4 coneTypeShadowView; // cos(inner), 0=point/1=spot, shadow view, flags
|
||||
float4 reserved;
|
||||
};
|
||||
struct ShadowViewGpu {
|
||||
column_major float4x4 viewProjection;
|
||||
float4 tileScaleOffset;
|
||||
float4 guardedClamp;
|
||||
float4 biasFlags;
|
||||
};
|
||||
[[vk::binding(0,1)]] StructuredBuffer<LightingHeader> lightingFrame;
|
||||
[[vk::binding(1,1)]] StructuredBuffer<LocalLightGpu> localLights;
|
||||
[[vk::binding(2,1)]] StructuredBuffer<ShadowViewGpu> shadowViews;
|
||||
[[vk::binding(3,1)]] Texture2D<float> localShadowAtlas;
|
||||
[shader("vertex")]
|
||||
VertexOutput vertexMain(VertexInput v) {
|
||||
VertexOutput o;
|
||||
@@ -32,6 +58,22 @@ VertexOutput vertexMain(VertexInput v) {
|
||||
}
|
||||
[shader("vertex")]
|
||||
float4 shadowMain(VertexInput v) : SV_Position { return mul(frame.lightViewProjection, float4(v.world,1)); }
|
||||
float3 directBRDF(float3 base, float rough, float metal, float3 n, float3 view, float3 l) {
|
||||
const float pi = 3.14159265;
|
||||
float nl = max(dot(n,l),0.0);
|
||||
if (nl <= 0.0) return float3(0);
|
||||
float3 halfVector = l + view;
|
||||
float halfLengthSquared = dot(halfVector, halfVector);
|
||||
float3 h = halfLengthSquared > 1e-8 ? halfVector * rsqrt(halfLengthSquared) : n;
|
||||
float nv=max(dot(n,view),0.001), nh=max(dot(n,h),0.0), vh=max(dot(view,h),0.0);
|
||||
float a=rough*rough, a2=a*a, denom=nh*nh*(a2-1.0)+1.0;
|
||||
float d=a2/(pi*denom*denom+0.0001);
|
||||
float k=(rough+1.0)*(rough+1.0)/8.0;
|
||||
float g=(nl/(nl*(1.0-k)+k))*(nv/(nv*(1.0-k)+k));
|
||||
float3 f0=lerp(float3(0.04),base,metal), fresnel=f0+(1.0-f0)*pow(1.0-vh,5.0);
|
||||
float3 spec=d*g*fresnel/max(4.0*nv*nl,0.001);
|
||||
return ((1.0-fresnel)*(1.0-metal)*base/pi+spec)*nl;
|
||||
}
|
||||
[shader("fragment")]
|
||||
float4 fragmentMain(VertexOutput v) : SV_Target {
|
||||
float4 sampled = colorMap.Sample(colorSampler, v.uv);
|
||||
@@ -41,28 +83,52 @@ float4 fragmentMain(VertexOutput v) : SV_Target {
|
||||
return v.color * sampled;
|
||||
}
|
||||
float4 base = v.color * sampled;
|
||||
const float pi = 3.14159265;
|
||||
float3 n=normalize(v.normal), l=normalize(-frame.lightDirection.xyz), view=normalize(frame.eye.xyz-v.world), h=normalize(l+view);
|
||||
float nl=max(dot(n,l),0.0), nv=max(dot(n,view),0.001), nh=max(dot(n,h),0.0), vh=max(dot(view,h),0.0);
|
||||
LightingHeader lighting = lightingFrame[0];
|
||||
float3 n=normalize(v.normal);
|
||||
float3 viewDelta=frame.eye.xyz-v.world;
|
||||
float viewLengthSquared=dot(viewDelta,viewDelta);
|
||||
float3 view=viewLengthSquared > 1e-8 ? viewDelta*rsqrt(viewLengthSquared) : n;
|
||||
float rough=clamp(v.material.x,0.08,1.0), metal=saturate(v.material.y);
|
||||
float a=rough*rough, a2=a*a, denom=nh*nh*(a2-1.0)+1.0;
|
||||
float d=a2/(pi*denom*denom+0.0001);
|
||||
float k=(rough+1.0)*(rough+1.0)/8.0;
|
||||
float g=(nl/(nl*(1.0-k)+k))*(nv/(nv*(1.0-k)+k));
|
||||
float3 f0=lerp(float3(0.04),base.rgb,metal), fresnel=f0+(1.0-f0)*pow(1.0-vh,5.0);
|
||||
float3 spec=d*g*fresnel/max(4.0*nv*nl,0.001);
|
||||
float4 lightClip=mul(frame.lightViewProjection,float4(v.world,1));
|
||||
float3 projected=lightClip.xyz/lightClip.w;
|
||||
float2 uv=projected.xy*.5+.5;
|
||||
float visibility=1.0;
|
||||
if(all(uv>=0.0)&&all(uv<=1.0)&&projected.z>=0.0&&projected.z<=1.0) {
|
||||
visibility=0.0;
|
||||
for(int y=-1;y<=1;++y) for(int x=-1;x<=1;++x) {
|
||||
float depth=shadowMap.SampleLevel(shadowSampler,uv+float2(x,y)/1024.0,0);
|
||||
visibility += projected.z-max(0.0008,0.003*(1.0-nl)) <= depth ? 1.0/9.0 : 0.0;
|
||||
float3 linear=base.rgb*.12;
|
||||
if (lighting.counts.y != 0 && lighting.sunDirectionIntensity.w > 0) {
|
||||
float3 l=normalize(-lighting.sunDirectionIntensity.xyz);
|
||||
float nl=max(dot(n,l),0.0);
|
||||
float visibility=1.0;
|
||||
if (lighting.counts.z != 0 && nl > 0) {
|
||||
float4 lightClip=mul(frame.lightViewProjection,float4(v.world,1));
|
||||
float3 projected=lightClip.xyz/lightClip.w;
|
||||
float2 uv=projected.xy*.5+.5;
|
||||
if(all(uv>=0.0)&&all(uv<=1.0)&&projected.z>=0.0&&projected.z<=1.0) {
|
||||
visibility=0.0;
|
||||
for(int y=-1;y<=1;++y) for(int x=-1;x<=1;++x) {
|
||||
float depth=shadowMap.SampleLevel(shadowSampler,uv+float2(x,y)/1024.0,0);
|
||||
visibility += projected.z-max(0.0008,0.003*(1.0-nl)) <= depth ? 1.0/9.0 : 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
linear += directBRDF(base.rgb, rough, metal, n, view, l) *
|
||||
lighting.sunColor.rgb * (lighting.sunDirectionIntensity.w * 3.0 * visibility);
|
||||
}
|
||||
for (uint i=0; i<lighting.counts.x; ++i) {
|
||||
LocalLightGpu light=localLights[i];
|
||||
float3 delta=light.positionRange.xyz-v.world;
|
||||
float distanceSquared=max(dot(delta,delta),1e-6);
|
||||
float distance=sqrt(distanceSquared);
|
||||
float range=max(light.positionRange.w,1e-4);
|
||||
if (distance >= range || light.colorIntensity.w <= 0) continue;
|
||||
float3 l=delta/distance;
|
||||
float relative=distance/range;
|
||||
float cutoff=1.0-relative*relative*relative*relative;
|
||||
float attenuation=cutoff*cutoff/(1.0+distanceSquared);
|
||||
if (light.coneTypeShadowView.y > 0.5) {
|
||||
float cosAngle=dot(-l,normalize(light.directionCosOuter.xyz));
|
||||
float denominator=max(light.coneTypeShadowView.x-light.directionCosOuter.w,1e-4);
|
||||
float cone=saturate((cosAngle-light.directionCosOuter.w)/denominator);
|
||||
attenuation *= cone*cone*(3.0-2.0*cone);
|
||||
}
|
||||
linear += directBRDF(base.rgb, rough, metal, n, view, l) *
|
||||
light.colorIntensity.rgb * (light.colorIntensity.w * attenuation);
|
||||
}
|
||||
float3 linear=base.rgb*.12 + ((1.0-fresnel)*(1.0-metal)*base.rgb/pi+spec)*nl*3.0*visibility;
|
||||
linear=linear/(1.0+linear);
|
||||
return float4(pow(max(linear,0),float3(1.0/2.2)),base.a);
|
||||
}
|
||||
|
||||
@@ -57,9 +57,9 @@ struct GpuFrameParameters {
|
||||
uint4 drawInfo; // x=visible ID range base; firstInstance is always zero
|
||||
};
|
||||
[[vk::push_constant]] ConstantBuffer<GpuFrameParameters> gpuFrame;
|
||||
[[vk::binding(0,1)]] StructuredBuffer<InstanceRecord> gfxInstances;
|
||||
[[vk::binding(1,1)]] StructuredBuffer<uint> gfxVisibleIds;
|
||||
[[vk::binding(2,1)]] StructuredBuffer<ViewRecord> gfxViews;
|
||||
[[vk::binding(0,2)]] StructuredBuffer<InstanceRecord> gfxInstances;
|
||||
[[vk::binding(1,2)]] StructuredBuffer<uint> gfxVisibleIds;
|
||||
[[vk::binding(2,2)]] StructuredBuffer<ViewRecord> gfxViews;
|
||||
|
||||
// All indirect commands use firstInstance=0. The raw Vulkan index avoids the
|
||||
// BaseInstance read that Slang adds for SV_InstanceID (DrawParameters feature).
|
||||
|
||||
@@ -156,6 +156,15 @@ void SchemaRegistry::validate_component(const Json& component) const {
|
||||
for (const auto& [id, value] : component["fields"].items())
|
||||
if (metadata["fields"].contains(id))
|
||||
validate_field(value, metadata["fields"][id]);
|
||||
if (type == "faset.light") {
|
||||
auto effective = default_fields(type);
|
||||
effective.update(component.at("fields"));
|
||||
if (effective.at("kind") == "spot")
|
||||
require(effective.at("inner_angle").get<double>() <=
|
||||
effective.at("outer_angle").get<double>(),
|
||||
"validation.light_cone",
|
||||
"Spotlight inner_angle must not exceed outer_angle");
|
||||
}
|
||||
}
|
||||
void SchemaRegistry::add_migration(const std::string& type, int from_version, Json rules) {
|
||||
require(contains(type) && from_version > 0 && from_version < schema(type).value("version", 1) &&
|
||||
@@ -267,7 +276,10 @@ SchemaRegistry builtin_schemas() {
|
||||
{"default", 0.7}, {"min", 0.001}, {"max", 1.55},
|
||||
{"unit", "radians"}}},
|
||||
{"casts_shadow", field("boolean", true)},
|
||||
{"shadow_priority", field("integer", 0)}});
|
||||
{"shadow_priority", Json{{"type", "integer"},
|
||||
{"default", 0},
|
||||
{"min", std::numeric_limits<int>::min()},
|
||||
{"max", std::numeric_limits<int>::max()}}}});
|
||||
for (int dimension : {2, 3}) {
|
||||
Json vector = dimension == 2 ? Json{0, 0} : Json{0, 0, 0};
|
||||
Json extents = dimension == 2 ? Json{0.5, 0.5} : Json{0.5, 0.5, 0.5};
|
||||
|
||||
@@ -397,7 +397,9 @@ render::Snapshot SceneView::build(const Json& scene, float aspect, CameraSetting
|
||||
for (const auto coordinate : local.position)
|
||||
if (!std::isfinite(coordinate))
|
||||
invalid("position");
|
||||
local.direction = normalized(direction(model, {0, 0, -1}), id, "direction");
|
||||
if (local.kind == render::LocalLight::Kind::Spot)
|
||||
local.direction = normalized(direction(model, {0, 0, -1}), id,
|
||||
"direction");
|
||||
local.color = color;
|
||||
local.intensity = intensity;
|
||||
local.range = number("range", 10);
|
||||
@@ -405,13 +407,19 @@ render::Snapshot SceneView::build(const Json& scene, float aspect, CameraSetting
|
||||
invalid("range");
|
||||
local.inner_angle = number("inner_angle", 0.35f);
|
||||
local.outer_angle = number("outer_angle", 0.7f);
|
||||
if (local.inner_angle < 0 || local.inner_angle > local.outer_angle ||
|
||||
local.outer_angle <= 0 || local.outer_angle >= std::numbers::pi_v<float> / 2)
|
||||
if (local.kind == render::LocalLight::Kind::Spot &&
|
||||
(local.inner_angle < 0 || local.inner_angle > local.outer_angle ||
|
||||
local.outer_angle <= 0 ||
|
||||
local.outer_angle >= std::numbers::pi_v<float> / 2))
|
||||
invalid("inner_angle/outer_angle");
|
||||
local.casts_shadow = castsShadow;
|
||||
if (fields.contains("shadow_priority")) {
|
||||
if (!fields.at("shadow_priority").is_number_integer())
|
||||
invalid("shadow_priority");
|
||||
const auto priority = fields.at("shadow_priority").get<double>();
|
||||
if (priority < std::numeric_limits<int>::min() ||
|
||||
priority > std::numeric_limits<int>::max())
|
||||
invalid("shadow_priority");
|
||||
local.shadow_priority = fields.at("shadow_priority").get<int>();
|
||||
}
|
||||
out.local_lights.push_back(std::move(local));
|
||||
|
||||
+192
-20
@@ -6,6 +6,7 @@
|
||||
#include <bit>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <faset/core/io.hpp>
|
||||
#include <faset/render/render_graph.hpp>
|
||||
@@ -14,6 +15,7 @@
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <numbers>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
@@ -72,6 +74,40 @@ struct ScenePush {
|
||||
std::array<std::uint32_t, 4> draw_info;
|
||||
};
|
||||
static_assert(sizeof(ScenePush) == 112);
|
||||
struct LightingHeaderGpu {
|
||||
std::array<std::uint32_t, 4> counts{};
|
||||
std::array<float, 4> sun_direction_intensity{};
|
||||
std::array<float, 4> sun_color{};
|
||||
std::array<float, 4> camera_forward_shadow_distance{};
|
||||
std::array<float, 4> cascade_splits{};
|
||||
};
|
||||
struct LocalLightGpu {
|
||||
std::array<float, 4> position_range{};
|
||||
std::array<float, 4> direction_cos_outer{};
|
||||
std::array<float, 4> color_intensity{};
|
||||
std::array<float, 4> cone_type_shadow_view{};
|
||||
std::array<float, 4> reserved{};
|
||||
};
|
||||
struct ShadowViewGpu {
|
||||
Mat4 view_projection{identity};
|
||||
std::array<float, 4> tile_scale_offset{};
|
||||
std::array<float, 4> guarded_clamp{};
|
||||
std::array<float, 4> bias_flags{};
|
||||
};
|
||||
static_assert(sizeof(LightingHeaderGpu) == 80 &&
|
||||
offsetof(LightingHeaderGpu, sun_direction_intensity) == 16 &&
|
||||
offsetof(LightingHeaderGpu, sun_color) == 32 &&
|
||||
offsetof(LightingHeaderGpu, camera_forward_shadow_distance) == 48 &&
|
||||
offsetof(LightingHeaderGpu, cascade_splits) == 64);
|
||||
static_assert(sizeof(LocalLightGpu) == 80 &&
|
||||
offsetof(LocalLightGpu, direction_cos_outer) == 16 &&
|
||||
offsetof(LocalLightGpu, color_intensity) == 32 &&
|
||||
offsetof(LocalLightGpu, cone_type_shadow_view) == 48 &&
|
||||
offsetof(LocalLightGpu, reserved) == 64);
|
||||
static_assert(sizeof(ShadowViewGpu) == 112 &&
|
||||
offsetof(ShadowViewGpu, tile_scale_offset) == 64 &&
|
||||
offsetof(ShadowViewGpu, guarded_clamp) == 80 &&
|
||||
offsetof(ShadowViewGpu, bias_flags) == 96);
|
||||
std::array<float, 4> point(const Mat4& m, std::array<float, 4> p) {
|
||||
std::array<float, 4> o{};
|
||||
for (int r = 0; r < 4; ++r)
|
||||
@@ -196,6 +232,7 @@ struct Renderer::Impl {
|
||||
std::vector<VkImageLayout> swap_layouts;
|
||||
Image color, depth, shadow;
|
||||
Buffer vertices, readback;
|
||||
Buffer lighting_header, lighting_locals, lighting_views;
|
||||
SceneResources scene;
|
||||
InstanceTracker instance_tracker;
|
||||
std::unordered_map<std::string, std::size_t> previous_lods;
|
||||
@@ -212,6 +249,9 @@ struct Renderer::Impl {
|
||||
std::unordered_map<const Texture*, CachedOpacity> opacity_cache;
|
||||
VkDescriptorSetLayout descriptor_layout{};
|
||||
VkDescriptorPool descriptor_pool{};
|
||||
VkDescriptorSetLayout lighting_layout{};
|
||||
VkDescriptorPool lighting_pool{};
|
||||
VkDescriptorSet lighting_set{};
|
||||
VkSampler shadow_sampler{}, color_sampler{};
|
||||
VkPipelineLayout pipeline_layout{};
|
||||
VkPipeline pipeline{}, ui_pipeline{}, shadow_pipeline{}, sprite_pipeline{};
|
||||
@@ -316,6 +356,9 @@ struct Renderer::Impl {
|
||||
destroy(scene.hzb[1]);
|
||||
destroy(vertices);
|
||||
destroy(readback);
|
||||
destroy(lighting_header);
|
||||
destroy(lighting_locals);
|
||||
destroy(lighting_views);
|
||||
destroy(color);
|
||||
destroy(depth);
|
||||
destroy(shadow);
|
||||
@@ -333,8 +376,12 @@ struct Renderer::Impl {
|
||||
vkDestroyPipelineLayout(device, pipeline_layout, nullptr);
|
||||
if (descriptor_pool)
|
||||
vkDestroyDescriptorPool(device, descriptor_pool, nullptr);
|
||||
if (lighting_pool)
|
||||
vkDestroyDescriptorPool(device, lighting_pool, nullptr);
|
||||
if (descriptor_layout)
|
||||
vkDestroyDescriptorSetLayout(device, descriptor_layout, nullptr);
|
||||
if (lighting_layout)
|
||||
vkDestroyDescriptorSetLayout(device, lighting_layout, nullptr);
|
||||
if (shadow_sampler)
|
||||
vkDestroySampler(device, shadow_sampler, nullptr);
|
||||
if (color_sampler)
|
||||
@@ -889,6 +936,31 @@ struct Renderer::Impl {
|
||||
pi.pPoolSizes = sizes;
|
||||
check(vkCreateDescriptorPool(device, &pi, nullptr, &descriptor_pool),
|
||||
"Create descriptor pool");
|
||||
std::array<VkDescriptorSetLayoutBinding, 4> lighting_bindings{};
|
||||
for (std::uint32_t i = 0; i < lighting_bindings.size(); ++i)
|
||||
lighting_bindings[i] = {i,
|
||||
i == 3 ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE
|
||||
: VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
||||
1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr};
|
||||
li.bindingCount = static_cast<std::uint32_t>(lighting_bindings.size());
|
||||
li.pBindings = lighting_bindings.data();
|
||||
check(vkCreateDescriptorSetLayout(device, &li, nullptr, &lighting_layout),
|
||||
"Create lighting descriptor layout");
|
||||
VkDescriptorPoolSize lighting_sizes[] = {
|
||||
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3}, {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1}};
|
||||
pi.flags = 0;
|
||||
pi.maxSets = 1;
|
||||
pi.poolSizeCount = 2;
|
||||
pi.pPoolSizes = lighting_sizes;
|
||||
check(vkCreateDescriptorPool(device, &pi, nullptr, &lighting_pool),
|
||||
"Create lighting descriptor pool");
|
||||
VkDescriptorSetAllocateInfo lighting_allocation{};
|
||||
lighting_allocation.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
||||
lighting_allocation.descriptorPool = lighting_pool;
|
||||
lighting_allocation.descriptorSetCount = 1;
|
||||
lighting_allocation.pSetLayouts = &lighting_layout;
|
||||
check(vkAllocateDescriptorSets(device, &lighting_allocation, &lighting_set),
|
||||
"Allocate lighting descriptors");
|
||||
VkSamplerCreateInfo si{};
|
||||
si.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
|
||||
si.magFilter = si.minFilter = VK_FILTER_NEAREST;
|
||||
@@ -1009,8 +1081,9 @@ struct Renderer::Impl {
|
||||
sizeof(Push)};
|
||||
VkPipelineLayoutCreateInfo li{};
|
||||
li.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
||||
li.setLayoutCount = 1;
|
||||
li.pSetLayouts = &descriptor_layout;
|
||||
const std::array<VkDescriptorSetLayout, 2> set_layouts{descriptor_layout, lighting_layout};
|
||||
li.setLayoutCount = static_cast<std::uint32_t>(set_layouts.size());
|
||||
li.pSetLayouts = set_layouts.data();
|
||||
li.pushConstantRangeCount = 1;
|
||||
li.pPushConstantRanges = &push;
|
||||
check(vkCreatePipelineLayout(device, &li, nullptr, &pipeline_layout),
|
||||
@@ -1220,14 +1293,14 @@ struct Renderer::Impl {
|
||||
layout.pBindings = hzb.data();
|
||||
check(vkCreateDescriptorSetLayout(device, &layout, nullptr, &scene.hzb_layout),
|
||||
"Create HZB descriptor layout");
|
||||
const std::array<VkDescriptorSetLayout, 2> scene_layouts{descriptor_layout,
|
||||
scene.graphics_layout};
|
||||
const std::array<VkDescriptorSetLayout, 3> scene_layouts{
|
||||
descriptor_layout, lighting_layout, scene.graphics_layout};
|
||||
VkPushConstantRange graphics_push{VK_SHADER_STAGE_VERTEX_BIT |
|
||||
VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
0, sizeof(ScenePush)};
|
||||
VkPipelineLayoutCreateInfo pipeline_info{};
|
||||
pipeline_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
||||
pipeline_info.setLayoutCount = 2;
|
||||
pipeline_info.setLayoutCount = static_cast<std::uint32_t>(scene_layouts.size());
|
||||
pipeline_info.pSetLayouts = scene_layouts.data();
|
||||
pipeline_info.pushConstantRangeCount = 1;
|
||||
pipeline_info.pPushConstantRanges = &graphics_push;
|
||||
@@ -1533,6 +1606,29 @@ struct Renderer::Impl {
|
||||
VkBufferUsageFlags usage = 0) {
|
||||
upload_scene_buffer(buffer, values.data(), values.size() * sizeof(T), usage);
|
||||
}
|
||||
void update_lighting_descriptors() {
|
||||
const std::array<VkDescriptorBufferInfo, 3> buffers{{
|
||||
{lighting_header.handle, 0, lighting_header.size},
|
||||
{lighting_locals.handle, 0, lighting_locals.size},
|
||||
{lighting_views.handle, 0, lighting_views.size}}};
|
||||
const VkDescriptorImageInfo atlas{VK_NULL_HANDLE, shadow.view,
|
||||
VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_OPTIMAL};
|
||||
std::array<VkWriteDescriptorSet, 4> writes{};
|
||||
for (std::uint32_t i = 0; i < writes.size(); ++i) {
|
||||
writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
writes[i].dstSet = lighting_set;
|
||||
writes[i].dstBinding = i;
|
||||
writes[i].descriptorCount = 1;
|
||||
writes[i].descriptorType = i == 3 ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE
|
||||
: VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||
if (i == 3)
|
||||
writes[i].pImageInfo = &atlas;
|
||||
else
|
||||
writes[i].pBufferInfo = &buffers[i];
|
||||
}
|
||||
vkUpdateDescriptorSets(device, static_cast<std::uint32_t>(writes.size()),
|
||||
writes.data(), 0, nullptr);
|
||||
}
|
||||
void update_scene_descriptors(bool occlusion) {
|
||||
auto write_buffers = [&](VkDescriptorSet set, std::span<const Buffer* const> buffers,
|
||||
std::uint32_t first_binding) {
|
||||
@@ -2017,15 +2113,87 @@ struct Renderer::Impl {
|
||||
VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
update_scene_descriptors(occlusion);
|
||||
}
|
||||
Vec3 direction = snapshot.light_direction;
|
||||
std::optional<SunLight> sun = snapshot.sun;
|
||||
if (!sun && !snapshot.authored_lights_present && snapshot.local_lights.empty())
|
||||
sun = SunLight{"legacy-sun", snapshot.light_direction, {1, 1, 1, 1}, 1, true};
|
||||
Vec3 direction = sun ? sun->direction : snapshot.light_direction;
|
||||
float length = std::sqrt(direction[0] * direction[0] + direction[1] * direction[1] +
|
||||
direction[2] * direction[2]);
|
||||
if (length < 1e-5f) {
|
||||
if (!std::isfinite(length) || length < 1e-5f) {
|
||||
if (sun && sun->stable_id != "legacy-sun")
|
||||
throw std::invalid_argument("Authored sun direction must be finite and nonzero");
|
||||
direction = {-.5f, -1, -.3f};
|
||||
length = std::sqrt(1.34f);
|
||||
}
|
||||
for (auto& v : direction)
|
||||
v /= length;
|
||||
LightingHeaderGpu lighting{};
|
||||
lighting.counts[1] = sun ? 1u : 0u;
|
||||
lighting.counts[2] = sun && sun->casts_shadow ? 1u : 0u;
|
||||
lighting.sun_direction_intensity = {direction[0], direction[1], direction[2],
|
||||
sun ? sun->intensity : 0};
|
||||
lighting.sun_color = sun ? sun->color : Color{0, 0, 0, 1};
|
||||
if (sun && (!std::isfinite(sun->intensity) || sun->intensity < 0 ||
|
||||
std::any_of(sun->color.begin(), sun->color.end(),
|
||||
[](float v) { return !std::isfinite(v) || v < 0; })))
|
||||
throw std::invalid_argument("Authored sun radiance must be finite and nonnegative");
|
||||
lighting.camera_forward_shadow_distance = {0, 0, -1, 80};
|
||||
if (snapshot.camera_frustum) {
|
||||
const auto& view = snapshot.camera_frustum->view;
|
||||
lighting.camera_forward_shadow_distance = {-view[2], -view[6], -view[10], 80};
|
||||
}
|
||||
auto sorted_lights = snapshot.local_lights;
|
||||
std::stable_sort(sorted_lights.begin(), sorted_lights.end(),
|
||||
[](const auto& a, const auto& b) { return a.stable_id < b.stable_id; });
|
||||
constexpr std::size_t max_local_lights = 128;
|
||||
std::vector<LocalLightGpu> gpu_lights;
|
||||
gpu_lights.reserve(std::min(sorted_lights.size(), max_local_lights));
|
||||
for (const auto& local : sorted_lights) {
|
||||
if (gpu_lights.size() == max_local_lights)
|
||||
break;
|
||||
const auto finite_color = std::all_of(local.color.begin(), local.color.end(),
|
||||
[](float v) { return std::isfinite(v) && v >= 0; });
|
||||
const auto finite_position = std::all_of(local.position.begin(), local.position.end(),
|
||||
[](float v) { return std::isfinite(v); });
|
||||
if (!finite_color || !finite_position || !std::isfinite(local.intensity) ||
|
||||
local.intensity < 0 || !std::isfinite(local.range) || local.range <= 0)
|
||||
throw std::invalid_argument("Local light radiance, position and range must be finite");
|
||||
if (local.kind == LocalLight::Kind::Spot &&
|
||||
(!std::isfinite(local.inner_angle) || !std::isfinite(local.outer_angle) ||
|
||||
local.inner_angle < 0 || local.inner_angle > local.outer_angle ||
|
||||
local.outer_angle >= std::numbers::pi_v<float> / 2))
|
||||
throw std::invalid_argument("Spotlight cone angles are invalid");
|
||||
auto spot_direction = local.direction;
|
||||
float spot_length = std::hypot(spot_direction[0], spot_direction[1],
|
||||
spot_direction[2]);
|
||||
if (!std::isfinite(spot_length) || spot_length < 1e-6f) {
|
||||
if (local.kind == LocalLight::Kind::Spot)
|
||||
throw std::invalid_argument("Spotlight direction must be finite and nonzero");
|
||||
spot_direction = {0, 0, -1};
|
||||
spot_length = 1;
|
||||
}
|
||||
for (auto& axis : spot_direction)
|
||||
axis /= spot_length;
|
||||
LocalLightGpu gpu{};
|
||||
gpu.position_range = {local.position[0], local.position[1], local.position[2],
|
||||
local.range};
|
||||
const bool spot = local.kind == LocalLight::Kind::Spot;
|
||||
gpu.direction_cos_outer = {spot_direction[0], spot_direction[1], spot_direction[2],
|
||||
spot ? std::cos(local.outer_angle) : 0.f};
|
||||
gpu.color_intensity = {local.color[0], local.color[1], local.color[2],
|
||||
local.intensity};
|
||||
gpu.cone_type_shadow_view = {spot ? std::cos(local.inner_angle) : 1.f,
|
||||
spot ? 1.f : 0.f, -1, 0};
|
||||
gpu_lights.push_back(gpu);
|
||||
}
|
||||
lighting.counts[0] = static_cast<std::uint32_t>(gpu_lights.size());
|
||||
if (gpu_lights.empty())
|
||||
gpu_lights.push_back({}); // Descriptors always point at a full initialized record.
|
||||
const ShadowViewGpu empty_shadow_view{};
|
||||
upload_scene_buffer(lighting_header, &lighting, sizeof(lighting), 0);
|
||||
upload_scene_vector(lighting_locals, gpu_lights);
|
||||
upload_scene_buffer(lighting_views, &empty_shadow_view, sizeof(empty_shadow_view), 0);
|
||||
update_lighting_descriptors();
|
||||
Vec3 light_eye{-direction[0] * 30, -direction[1] * 30, -direction[2] * 30};
|
||||
Vec3 light_up = std::abs(direction[1]) > .98f ? Vec3{0, 0, 1} : Vec3{0, 1, 0};
|
||||
Push push{multiply(orthographic(-20, 20, -20, 20, .1f, 80),
|
||||
@@ -2078,6 +2246,12 @@ struct Renderer::Impl {
|
||||
vkCmdSetViewport(command, 0, 1, &viewport);
|
||||
vkCmdSetScissor(command, 0, 1, &scissor);
|
||||
};
|
||||
auto bind_material = [&](VkDescriptorSet material) {
|
||||
const std::array<VkDescriptorSet, 2> sets{material, lighting_set};
|
||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout,
|
||||
0, static_cast<std::uint32_t>(sets.size()), sets.data(),
|
||||
0, nullptr);
|
||||
};
|
||||
auto draw_transparent = [&] {
|
||||
if (transparent_batches.empty())
|
||||
return;
|
||||
@@ -2088,8 +2262,7 @@ struct Renderer::Impl {
|
||||
0, sizeof(push), &push);
|
||||
for (auto batch : transparent_batches) {
|
||||
auto descriptor = textures.at(batch.texture).descriptor;
|
||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
pipeline_layout, 0, 1, &descriptor, 0, nullptr);
|
||||
bind_material(descriptor);
|
||||
vkCmdDraw(command, batch.count, 1, batch.first, 0);
|
||||
++statistics.draw_calls;
|
||||
}
|
||||
@@ -2102,8 +2275,7 @@ struct Renderer::Impl {
|
||||
sizeof(push), &push);
|
||||
for (auto batch : sprite_batches) {
|
||||
auto descriptor = textures.at(batch.texture).descriptor;
|
||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout,
|
||||
0, 1, &descriptor, 0, nullptr);
|
||||
bind_material(descriptor);
|
||||
vkCmdDraw(command, batch.count, 1, batch.first, 0);
|
||||
++statistics.draw_calls;
|
||||
}
|
||||
@@ -2125,8 +2297,7 @@ struct Renderer::Impl {
|
||||
continue;
|
||||
vkCmdSetScissor(command, 0, 1, &scissor);
|
||||
auto descriptor = textures.at(batch.texture).descriptor;
|
||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout,
|
||||
0, 1, &descriptor, 0, nullptr);
|
||||
bind_material(descriptor);
|
||||
vkCmdDraw(command, batch.count, 1, batch.first, 0);
|
||||
++statistics.draw_calls;
|
||||
}
|
||||
@@ -2313,8 +2484,7 @@ struct Renderer::Impl {
|
||||
vkCmdPushConstants(command, pipeline_layout,
|
||||
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0,
|
||||
sizeof(push), &push);
|
||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1,
|
||||
&white_descriptor, 0, nullptr);
|
||||
bind_material(white_descriptor);
|
||||
if (gpu_active && !gpu_frame.bins.empty()) {
|
||||
VkDeviceSize scene_offset{};
|
||||
vkCmdBindVertexBuffers(command, 0, 1, &scene.vertices.handle, &scene_offset);
|
||||
@@ -2322,7 +2492,7 @@ struct Renderer::Impl {
|
||||
scene.graphics_pipeline);
|
||||
for (std::uint32_t bin = 0; bin < gpu_frame.bins.size(); ++bin) {
|
||||
auto descriptor = textures.at(gpu_frame.textures[bin]).descriptor;
|
||||
const std::array<VkDescriptorSet, 2> sets{descriptor,
|
||||
const std::array<VkDescriptorSet, 3> sets{descriptor, lighting_set,
|
||||
scene.graphics_main};
|
||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
scene.graphics_pipeline_layout, 0, sets.size(),
|
||||
@@ -2346,8 +2516,7 @@ struct Renderer::Impl {
|
||||
}
|
||||
for (auto batch : scene_batches) {
|
||||
auto descriptor = textures.at(batch.texture).descriptor;
|
||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout,
|
||||
0, 1, &descriptor, 0, nullptr);
|
||||
bind_material(descriptor);
|
||||
vkCmdDraw(command, batch.count, 1, batch.first, 0);
|
||||
++statistics.draw_calls;
|
||||
}
|
||||
@@ -2446,7 +2615,7 @@ struct Renderer::Impl {
|
||||
scene.graphics_pipeline);
|
||||
for (std::uint32_t bin = 0; bin < gpu_frame.bins.size(); ++bin) {
|
||||
auto descriptor = textures.at(gpu_frame.textures[bin]).descriptor;
|
||||
const std::array<VkDescriptorSet, 2> sets{descriptor,
|
||||
const std::array<VkDescriptorSet, 3> sets{descriptor, lighting_set,
|
||||
scene.graphics_post};
|
||||
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
scene.graphics_pipeline_layout, 0,
|
||||
@@ -2618,7 +2787,10 @@ struct Renderer::Impl {
|
||||
++statistics.frame;
|
||||
statistics.gpu_allocated_bytes = vertices.allocation_size + readback.allocation_size +
|
||||
color.allocation_size + depth.allocation_size +
|
||||
shadow.allocation_size;
|
||||
shadow.allocation_size +
|
||||
lighting_header.allocation_size +
|
||||
lighting_locals.allocation_size +
|
||||
lighting_views.allocation_size;
|
||||
statistics.texture_count = static_cast<std::uint32_t>(textures.size());
|
||||
for (const auto& [_, texture] : textures)
|
||||
statistics.gpu_allocated_bytes += texture.image.allocation_size;
|
||||
|
||||
@@ -34,13 +34,21 @@ void validate_layout(const Json& layout, std::string_view entry) {
|
||||
const bool fragment = entry == "fragmentMain";
|
||||
require(layout.at("stage") == (fragment ? "fragment" : "vertex"), "shader stage changed");
|
||||
const auto& descriptors = layout.at("descriptors");
|
||||
require(descriptors.is_array() && descriptors.size() == 4, "descriptor count changed");
|
||||
require(descriptors.is_array() && descriptors.size() == 8, "descriptor count changed");
|
||||
for (std::size_t i = 0; i < descriptors.size(); ++i) {
|
||||
const auto& binding = descriptors[i];
|
||||
require(binding.at("set") == 0 && binding.at("binding") == i && binding.at("count") == 1,
|
||||
const auto set = i < 4 ? 0 : 1;
|
||||
const auto slot = i % 4;
|
||||
require(binding.at("set") == set && binding.at("binding") == slot &&
|
||||
binding.at("count") == 1,
|
||||
"descriptor set, binding or array count changed");
|
||||
require(binding.at("type") == (i % 2 ? "sampler" : "sampled_image_2d"),
|
||||
const auto* expected_type = set == 0 ? (slot % 2 ? "sampler" : "sampled_image_2d")
|
||||
: slot == 3 ? "sampled_image_2d" : "storage_buffer";
|
||||
require(binding.at("type") == expected_type,
|
||||
"descriptor type changed");
|
||||
if (set == 1 && slot < 3)
|
||||
require(binding.at("element_stride") == (slot == 2 ? 112 : 80),
|
||||
"lighting storage record stride changed");
|
||||
require(fragment || !binding.at("used").get<bool>(),
|
||||
"vertex texture bindings are unsupported");
|
||||
}
|
||||
@@ -98,7 +106,7 @@ void validate_gpu_layout(const Json& layout, std::string_view entry) {
|
||||
const std::array<int, 3> graphics_strides{224, 4, 208};
|
||||
for (std::size_t i = 0; i < expected_count; ++i) {
|
||||
const auto& binding = descriptors[i];
|
||||
require(binding.at("set") == (graphics ? 1 : 0) && binding.at("binding") == i &&
|
||||
require(binding.at("set") == (graphics ? 2 : 0) && binding.at("binding") == i &&
|
||||
binding.at("count") == 1,
|
||||
"GPU descriptor set, binding or count changed");
|
||||
const int stride = graphics ? graphics_strides[i] : hzb ? 0 : compute_strides[i];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <faset/authoring/templates.hpp>
|
||||
#include <faset/authoring/transforms.hpp>
|
||||
#include <faset/core/io.hpp>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
|
||||
#define CHECK(x) \
|
||||
@@ -44,6 +45,13 @@ int main() {
|
||||
light_component["fields"]["range"] = 10;
|
||||
light_component["fields"]["intensity"] = -1;
|
||||
fails([&] { schemas.validate_component(light_component); }, "validation.minimum");
|
||||
light_component["fields"] = {{"kind", "spot"}, {"inner_angle", 0.9}};
|
||||
fails([&] { schemas.validate_component(light_component); }, "validation.light_cone");
|
||||
light_component["fields"] = {{"kind", "spot"},
|
||||
{"inner_angle", 0.2},
|
||||
{"outer_angle", 0.5},
|
||||
{"shadow_priority", std::int64_t{2147483648}}};
|
||||
fails([&] { schemas.validate_component(light_component); }, "validation.maximum");
|
||||
AuthoringService service(root, schemas);
|
||||
auto created = service.create("Courtyard", 3);
|
||||
const std::string id = created["id"];
|
||||
|
||||
@@ -59,6 +59,15 @@ int main() {
|
||||
faset::atomic_write_json(reflection_file, metadata);
|
||||
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
||||
"A consistently rehashed but incompatible GPU record stride must be rejected");
|
||||
faset::atomic_write_json(reflection_file,
|
||||
faset::read_json(original / "gpuPostCullMain.reflection.json"));
|
||||
reflection_file = temporary / "gpuVertexMain.reflection.json";
|
||||
metadata = faset::read_json(original / "gpuVertexMain.reflection.json");
|
||||
metadata["layout"]["descriptors"][0]["set"] = 1;
|
||||
metadata["layout_fingerprint"] = faset::sha256(metadata["layout"].dump());
|
||||
faset::atomic_write_json(reflection_file, metadata);
|
||||
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
||||
"GPU graphics scene buffers must stay in descriptor set two");
|
||||
fs::remove(temporary / "gpuHzbMain.spv");
|
||||
must_reject([&] { (void)faset::render::detail::load_gpu_shader_bundle(temporary); },
|
||||
"Missing P2 entry must be rejected");
|
||||
|
||||
@@ -59,6 +59,27 @@ int main() {
|
||||
const auto original_reflection = read_text(bundle / "fragmentMain.reflection.json");
|
||||
const auto original_fingerprint = Json::parse(original_reflection).at("layout_fingerprint");
|
||||
render::validate_shader_bundle(bundle);
|
||||
auto bad_lighting_stride = Json::parse(original_reflection);
|
||||
auto& lighting_descriptors = bad_lighting_stride["layout"]["descriptors"];
|
||||
bool found_local_buffer = false;
|
||||
for (auto& descriptor : lighting_descriptors)
|
||||
if (descriptor["set"] == 1 && descriptor["binding"] == 1) {
|
||||
descriptor["element_stride"] = 96;
|
||||
found_local_buffer = true;
|
||||
}
|
||||
require(found_local_buffer, "Lighting stride fixture exists");
|
||||
bad_lighting_stride["layout_fingerprint"] =
|
||||
sha256(bad_lighting_stride["layout"].dump());
|
||||
atomic_write_json(bundle / "fragmentMain.reflection.json", bad_lighting_stride);
|
||||
bool rejected_lighting_stride = false;
|
||||
try {
|
||||
render::validate_shader_bundle(bundle);
|
||||
} catch (const std::exception&) {
|
||||
rejected_lighting_stride = true;
|
||||
}
|
||||
require(rejected_lighting_stride,
|
||||
"Rehashed incompatible local-light element stride must be rejected");
|
||||
atomic_write(bundle / "fragmentMain.reflection.json", original_reflection);
|
||||
render::RendererConfig configuration;
|
||||
configuration.width = configuration.height = 64;
|
||||
configuration.headless = true;
|
||||
@@ -161,6 +182,18 @@ int main() {
|
||||
atomic_write(bundle / "fragmentMain.spv", "damaged bytecode");
|
||||
retained();
|
||||
restore();
|
||||
auto incompatible = original_source;
|
||||
auto at = incompatible.find(" float4 reserved;");
|
||||
require(at != std::string::npos, "Local-light stride fixture exists");
|
||||
incompatible.replace(at, std::string(" float4 reserved;").size(),
|
||||
" float4 reserved;\n float4 incompatibleExtraLane;");
|
||||
atomic_write(source, incompatible);
|
||||
require(compile(source, bundle) == 0, "Compile incompatible light-buffer stride");
|
||||
require(read_json(bundle / "fragmentMain.reflection.json").at("layout_fingerprint") !=
|
||||
original_fingerprint,
|
||||
"Lighting stride edit changes normalized layout fingerprint");
|
||||
retained();
|
||||
restore();
|
||||
auto malformed = original_spirv;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
malformed[20 + i] = 0; // zero-word SPIR-V instruction
|
||||
@@ -170,8 +203,8 @@ int main() {
|
||||
atomic_write_json(bundle / "fragmentMain.reflection.json", metadata);
|
||||
retained();
|
||||
restore();
|
||||
auto incompatible = original_source;
|
||||
auto at = incompatible.find("[[vk::binding(2,0)]]");
|
||||
incompatible = original_source;
|
||||
at = incompatible.find("[[vk::binding(2,0)]]");
|
||||
require(at != std::string::npos, "Shader descriptor fixture exists");
|
||||
incompatible.replace(at, std::string("[[vk::binding(2,0)]]").size(),
|
||||
"[[vk::binding(7,0)]]");
|
||||
|
||||
@@ -134,6 +134,65 @@ int main(int argc, char** argv) {
|
||||
renderer.render(scene);
|
||||
pixels = renderer.pixels();
|
||||
require(pixels[index + 2] > 220, "Texture revision upload");
|
||||
Snapshot two_lights;
|
||||
two_lights.eye = {0, 0, 6};
|
||||
two_lights.projection = perspective(.85f, 320.f / 240.f, .1f, 30.f);
|
||||
two_lights.view_projection =
|
||||
multiply(two_lights.projection, look_at(two_lights.eye, {0, 0, 0}));
|
||||
two_lights.authored_lights_present = true;
|
||||
two_lights.draws.push_back(
|
||||
{cube_mesh(), transform({-1.4f, 0, 0}), {.5f, .5f, .5f, 1}, .6f, 0, false});
|
||||
two_lights.draws.back().instance_key = "left-light-receiver";
|
||||
two_lights.draws.push_back(
|
||||
{cube_mesh(), transform({1.4f, 0, 0}), {.5f, .5f, .5f, 1}, .6f, 0, false});
|
||||
two_lights.draws.back().instance_key = "right-light-receiver";
|
||||
two_lights.ui_quads.push_back({8, 8, 40, 20, {.8f, .1f, .15f, 1}});
|
||||
for (auto mode : {VisibilityMode::Direct, VisibilityMode::GpuFrustum}) {
|
||||
renderer.set_visibility_mode(mode);
|
||||
renderer.render(two_lights);
|
||||
const auto dark = renderer.pixels();
|
||||
require(renderer.stats().validation_errors == 0,
|
||||
"Zero-local-light descriptors are initialized");
|
||||
auto legacy_lights = two_lights;
|
||||
legacy_lights.authored_lights_present = false;
|
||||
renderer.render(legacy_lights);
|
||||
const auto legacy = renderer.pixels();
|
||||
const auto left = (120 * 320 + 99) * 4;
|
||||
require(legacy[left] > dark[left] + 15,
|
||||
"Authored-light presence suppresses the legacy sun even without a local light");
|
||||
two_lights.local_lights = {
|
||||
{LocalLight::Kind::Point, "red", {-1.4f, 0, 1.4f}, {0, 0, -1},
|
||||
{1, 0, 0, 1}, 8, 2.2f, .35f, .7f, false, 0},
|
||||
{LocalLight::Kind::Point, "blue", {1.4f, 0, 1.4f}, {0, 0, -1},
|
||||
{0, 0, 1, 1}, 8, 2.5f, .35f, .7f, false, 0}};
|
||||
renderer.render(two_lights);
|
||||
const auto lit = renderer.pixels();
|
||||
const auto right = (120 * 320 + 221) * 4;
|
||||
const auto ui = (10 * 320 + 10) * 4;
|
||||
require(lit[left] > dark[left] + 20 && lit[right + 2] > dark[right + 2] + 20,
|
||||
"Separated red and blue point lights illuminate their receivers");
|
||||
require(std::abs(int(lit[left + 2]) - int(dark[left + 2])) < 6 &&
|
||||
std::abs(int(lit[right]) - int(dark[right])) < 6,
|
||||
"Local light range keeps the opposite colored light off each receiver");
|
||||
for (int channel = 0; channel < 4; ++channel)
|
||||
require(lit[ui + channel] == dark[ui + channel],
|
||||
"Lighting changes leave UI tint unchanged");
|
||||
require(renderer.stats().validation_errors == 0,
|
||||
"Direct and GPU local lighting report no Vulkan errors");
|
||||
if (mode == VisibilityMode::GpuFrustum)
|
||||
require(renderer.stats().effective_visibility_mode == VisibilityMode::GpuFrustum,
|
||||
"Local light image test actually exercises the GPU visibility path");
|
||||
two_lights.local_lights[1].kind = LocalLight::Kind::Spot;
|
||||
renderer.render(two_lights);
|
||||
const auto aimed = renderer.pixels();
|
||||
two_lights.local_lights[1].direction = {1, 0, 0};
|
||||
renderer.render(two_lights);
|
||||
const auto turned = renderer.pixels();
|
||||
require(aimed[right + 2] > turned[right + 2] + 20,
|
||||
"Spotlight cone direction changes receiver illumination");
|
||||
two_lights.local_lights.clear();
|
||||
}
|
||||
renderer.set_visibility_mode(VisibilityMode::Direct);
|
||||
if (argc > 2)
|
||||
renderer.capture(argv[2]);
|
||||
renderer.resize(400, 300);
|
||||
|
||||
@@ -134,6 +134,18 @@ void lightingExtraction(faset::player::SceneView& view) {
|
||||
rejectsContaining([&] { view.build(scene, 1); }, "bad-light", "inner_angle");
|
||||
scene["entities"][0]["components"][0]["fields"] = {{"kind", "area"}};
|
||||
rejectsContaining([&] { view.build(scene, 1); }, "bad-light", "kind");
|
||||
scene["entities"][0]["components"][0]["fields"] =
|
||||
{{"kind", "point"}, {"shadow_priority", std::int64_t{2147483648}}};
|
||||
rejectsContaining([&] { view.build(scene, 1); }, "bad-light", "shadow_priority");
|
||||
scene["entities"][0]["components"][0]["fields"] = {{"kind", "point"}};
|
||||
scene["entities"][0]["components"].insert(
|
||||
scene["entities"][0]["components"].begin(),
|
||||
component("faset.transform", {{"scale", {1, 1, 0}}}));
|
||||
const auto flatPoint = view.build(scene, 1);
|
||||
check(flatPoint.local_lights.size() == 1 &&
|
||||
flatPoint.local_lights[0].kind == faset::render::LocalLight::Kind::Point,
|
||||
"Point light accepts a zero Z scale because it needs only a position");
|
||||
scene["entities"][0]["components"].erase(scene["entities"][0]["components"].begin());
|
||||
scene["entities"][0]["components"][0]["fields"] =
|
||||
{{"kind", "point"},
|
||||
{"color", Json::array({1, std::numeric_limits<double>::quiet_NaN(), 1, 1})}};
|
||||
|
||||
@@ -36,6 +36,35 @@ def parameter(name: str, index: int, shape: str, access: str, stride: int | None
|
||||
|
||||
|
||||
class ReflectionTests(unittest.TestCase):
|
||||
def test_graphics_lighting_abi(self):
|
||||
compiler = os.environ["FASET_TEST_SLANGC"]
|
||||
with tempfile.TemporaryDirectory(prefix="faset-lighting-abi-") as directory:
|
||||
for source, entry, defines in (
|
||||
("baseline.slang", "fragmentMain", []),
|
||||
("gpu_scene.slang", "gpuVertexMain", ["--define", "FASET_GPU_GRAPHICS=1"]),
|
||||
):
|
||||
process = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "--compiler", compiler, "--source",
|
||||
str(SCRIPT.parents[1] / "shaders" / source), "--entry", entry,
|
||||
*defines, "--output", directory],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
self.assertEqual(process.returncode, 0, process.stderr)
|
||||
fragment = json.loads((Path(directory) / "fragmentMain.reflection.json").read_text())
|
||||
gpu_vertex = json.loads((Path(directory) / "gpuVertexMain.reflection.json").read_text())
|
||||
lighting = {
|
||||
(d["set"], d["binding"]): (d["type"], d.get("element_stride"))
|
||||
for d in fragment["layout"]["descriptors"]
|
||||
}
|
||||
self.assertEqual([lighting[1, i] for i in range(4)],
|
||||
[("storage_buffer", 80), ("storage_buffer", 80),
|
||||
("storage_buffer", 112), ("sampled_image_2d", None)])
|
||||
graphics = {
|
||||
(d["set"], d["binding"]): d["element_stride"]
|
||||
for d in gpu_vertex["layout"]["descriptors"]
|
||||
}
|
||||
self.assertEqual([graphics[2, i] for i in range(3)], [224, 4, 208])
|
||||
|
||||
def test_gpu_vertex_paths_do_not_require_shader_draw_parameters(self):
|
||||
# SV_InstanceID makes Slang subtract BaseInstance and emit DrawParameters.
|
||||
# Our indirect commands always use firstInstance=0, so the Vulkan instance
|
||||
|
||||
Reference in New Issue
Block a user