Bind shared lighting ABI and shade authored local lights
This commit is contained in:
@@ -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