Expose P2 GPU visibility modes in standalone Player
This commit is contained in:
+43
-7
@@ -35,6 +35,7 @@ struct ProfileSample {
|
|||||||
std::uint64_t gpuAllocatedBytes{};
|
std::uint64_t gpuAllocatedBytes{};
|
||||||
std::uint32_t textureCount{};
|
std::uint32_t textureCount{};
|
||||||
bool physicsDebug{};
|
bool physicsDebug{};
|
||||||
|
bool gpuVisibilityActive{};
|
||||||
};
|
};
|
||||||
Json distribution(std::vector<double> values) {
|
Json distribution(std::vector<double> values) {
|
||||||
if (values.empty())
|
if (values.empty())
|
||||||
@@ -80,7 +81,8 @@ Json profileFrames(const std::vector<ProfileSample>& samples) {
|
|||||||
{"vertices", sample.vertices},
|
{"vertices", sample.vertices},
|
||||||
{"gpu_allocated_bytes", sample.gpuAllocatedBytes},
|
{"gpu_allocated_bytes", sample.gpuAllocatedBytes},
|
||||||
{"texture_count", sample.textureCount},
|
{"texture_count", sample.textureCount},
|
||||||
{"physics_debug", sample.physicsDebug}});
|
{"physics_debug", sample.physicsDebug},
|
||||||
|
{"gpu_visibility_active", sample.gpuVisibilityActive}});
|
||||||
}
|
}
|
||||||
return {{"samples", std::move(frames)},
|
return {{"samples", std::move(frames)},
|
||||||
{"summary_ms",
|
{"summary_ms",
|
||||||
@@ -167,7 +169,8 @@ faset::runtime::RuntimeConfig simulationConfig(const nlohmann::json& scene) {
|
|||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
void validatePackagedShaders(const std::filesystem::path& directory) {
|
void validatePackagedShaders(const std::filesystem::path& directory,
|
||||||
|
faset::render::VisibilityMode visibilityMode) {
|
||||||
const auto shaders = directory / "shaders";
|
const auto shaders = directory / "shaders";
|
||||||
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain"})
|
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain"})
|
||||||
for (const auto* extension : {".spv", ".reflection.json"}) {
|
for (const auto* extension : {".spv", ".reflection.json"}) {
|
||||||
@@ -177,6 +180,8 @@ void validatePackagedShaders(const std::filesystem::path& directory) {
|
|||||||
faset::path_to_utf8(path));
|
faset::path_to_utf8(path));
|
||||||
}
|
}
|
||||||
faset::render::validate_shader_bundle(shaders);
|
faset::render::validate_shader_bundle(shaders);
|
||||||
|
if (visibilityMode != faset::render::VisibilityMode::Direct)
|
||||||
|
faset::render::validate_gpu_shader_bundle(shaders);
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
int player_main(int argc, char** argv) {
|
int player_main(int argc, char** argv) {
|
||||||
@@ -185,6 +190,8 @@ int player_main(int argc, char** argv) {
|
|||||||
std::filesystem::path scenePath, assetsPath, capturePath, controlPath, profilePath,
|
std::filesystem::path scenePath, assetsPath, capturePath, controlPath, profilePath,
|
||||||
projectRoot;
|
projectRoot;
|
||||||
bool headless = false, validateOnly = false, debugPhysics = false, watchLua = false;
|
bool headless = false, validateOnly = false, debugPhysics = false, watchLua = false;
|
||||||
|
auto visibilityMode = faset::render::VisibilityMode::Direct;
|
||||||
|
std::string visibilityName = "direct";
|
||||||
std::uint64_t maximumFrames = 0;
|
std::uint64_t maximumFrames = 0;
|
||||||
std::set<std::string> options;
|
std::set<std::string> options;
|
||||||
for (int i = 1; i < argc; ++i) {
|
for (int i = 1; i < argc; ++i) {
|
||||||
@@ -194,7 +201,7 @@ int player_main(int argc, char** argv) {
|
|||||||
<< "faset_player [--scene PATH] [--assets CACHE] [--frames N] "
|
<< "faset_player [--scene PATH] [--assets CACHE] [--frames N] "
|
||||||
"[--headless] [--capture PATH.ppm] [--validate] [--control PATH] "
|
"[--headless] [--capture PATH.ppm] [--validate] [--control PATH] "
|
||||||
"[--profile PATH.json] [--debug-physics] [--project ROOT] "
|
"[--profile PATH.json] [--debug-physics] [--project ROOT] "
|
||||||
"[--watch-lua]\n"
|
"[--watch-lua] [--visibility direct|gpu-frustum|gpu-occlusion]\n"
|
||||||
"No --scene: open scene.fscene beside the executable. CACHE contains "
|
"No --scene: open scene.fscene beside the executable. CACHE contains "
|
||||||
"assets/<id>/.\n"
|
"assets/<id>/.\n"
|
||||||
"Headless uses offscreen Vulkan; --frames uses the configured fixed "
|
"Headless uses offscreen Vulkan; --frames uses the configured fixed "
|
||||||
@@ -209,6 +216,9 @@ int player_main(int argc, char** argv) {
|
|||||||
"reload-lua): the scene restarts, runtime state is not preserved.\n"
|
"reload-lua): the scene restarts, runtime state is not preserved.\n"
|
||||||
"--profile requires explicit --frames 1..100000; measured durations "
|
"--profile requires explicit --frames 1..100000; measured durations "
|
||||||
"include the first frame and renderer GPU waits/readback.\n"
|
"include the first frame and renderer GPU waits/readback.\n"
|
||||||
|
"--visibility selects the renderer for this Player run; Direct is "
|
||||||
|
"the default. GPU modes require their packaged shader bundle and "
|
||||||
|
"device capabilities.\n"
|
||||||
"Keys: A/D horizontal, W/S vertical, Space jump, E interact, P pause, "
|
"Keys: A/D horizontal, W/S vertical, Space jump, E interact, P pause, "
|
||||||
"N single-step, F3 physics boxes, Escape quit.\n";
|
"N single-step, F3 physics boxes, Escape quit.\n";
|
||||||
return 0;
|
return 0;
|
||||||
@@ -232,6 +242,18 @@ int player_main(int argc, char** argv) {
|
|||||||
profilePath = faset::path_from_utf8(value());
|
profilePath = faset::path_from_utf8(value());
|
||||||
else if (arg == "--project")
|
else if (arg == "--project")
|
||||||
projectRoot = faset::path_from_utf8(value());
|
projectRoot = faset::path_from_utf8(value());
|
||||||
|
else if (arg == "--visibility") {
|
||||||
|
visibilityName = value();
|
||||||
|
if (visibilityName == "direct")
|
||||||
|
visibilityMode = faset::render::VisibilityMode::Direct;
|
||||||
|
else if (visibilityName == "gpu-frustum")
|
||||||
|
visibilityMode = faset::render::VisibilityMode::GpuFrustum;
|
||||||
|
else if (visibilityName == "gpu-occlusion")
|
||||||
|
visibilityMode = faset::render::VisibilityMode::GpuOcclusion;
|
||||||
|
else
|
||||||
|
throw std::invalid_argument("--visibility must be direct, gpu-frustum, "
|
||||||
|
"or gpu-occlusion");
|
||||||
|
}
|
||||||
else if (arg == "--frames")
|
else if (arg == "--frames")
|
||||||
maximumFrames = count(value());
|
maximumFrames = count(value());
|
||||||
else if (arg == "--headless")
|
else if (arg == "--headless")
|
||||||
@@ -302,7 +324,7 @@ int player_main(int argc, char** argv) {
|
|||||||
const auto sceneReadFinished = Clock::now();
|
const auto sceneReadFinished = Clock::now();
|
||||||
if (scenePath.extension() == ".fscene" &&
|
if (scenePath.extension() == ".fscene" &&
|
||||||
std::filesystem::equivalent(scenePath.parent_path(), executableRoot))
|
std::filesystem::equivalent(scenePath.parent_path(), executableRoot))
|
||||||
validatePackagedShaders(executableRoot);
|
validatePackagedShaders(executableRoot, visibilityMode);
|
||||||
if (validateOnly) {
|
if (validateOnly) {
|
||||||
if (!capturePath.empty() || !controlPath.empty())
|
if (!capturePath.empty() || !controlPath.empty())
|
||||||
throw std::invalid_argument("--validate cannot capture or control a running game");
|
throw std::invalid_argument("--validate cannot capture or control a running game");
|
||||||
@@ -342,8 +364,14 @@ int player_main(int argc, char** argv) {
|
|||||||
printGameplayLogs();
|
printGameplayLogs();
|
||||||
faset::player::SceneView view(assetsPath);
|
faset::player::SceneView view(assetsPath);
|
||||||
const auto rendererStarted = Clock::now();
|
const auto rendererStarted = Clock::now();
|
||||||
faset::render::Renderer renderer(
|
faset::render::RendererConfig renderConfig;
|
||||||
{1280, 720, document.value("name", std::string("Faset Player")), headless, true});
|
renderConfig.width = 1280;
|
||||||
|
renderConfig.height = 720;
|
||||||
|
renderConfig.title = document.value("name", std::string("Faset Player"));
|
||||||
|
renderConfig.headless = headless;
|
||||||
|
renderConfig.validation = true;
|
||||||
|
renderConfig.visibility_mode = visibilityMode;
|
||||||
|
faset::render::Renderer renderer(renderConfig);
|
||||||
const auto rendererReady = Clock::now();
|
const auto rendererReady = Clock::now();
|
||||||
std::vector<ProfileSample> profile;
|
std::vector<ProfileSample> profile;
|
||||||
if (!profilePath.empty())
|
if (!profilePath.empty())
|
||||||
@@ -528,6 +556,10 @@ int player_main(int argc, char** argv) {
|
|||||||
printGameplayLogs();
|
printGameplayLogs();
|
||||||
const auto renderStarted = Clock::now();
|
const auto renderStarted = Clock::now();
|
||||||
renderer.render(snapshot);
|
renderer.render(snapshot);
|
||||||
|
if (frames == 0 && visibilityMode != faset::render::VisibilityMode::Direct &&
|
||||||
|
!renderer.stats().gpu_visibility_active)
|
||||||
|
std::cerr << "Requested GPU visibility is unavailable on this device; "
|
||||||
|
"using Direct rendering.\n";
|
||||||
const auto frameFinished = Clock::now();
|
const auto frameFinished = Clock::now();
|
||||||
if (frames == 0)
|
if (frames == 0)
|
||||||
firstFrameMs = milliseconds(started, frameFinished);
|
firstFrameMs = milliseconds(started, frameFinished);
|
||||||
@@ -539,7 +571,8 @@ int player_main(int argc, char** argv) {
|
|||||||
milliseconds(simulationFinished, snapshotFinished),
|
milliseconds(simulationFinished, snapshotFinished),
|
||||||
milliseconds(renderStarted, frameFinished), measured.cpu_ms, measured.gpu_ms,
|
milliseconds(renderStarted, frameFinished), measured.cpu_ms, measured.gpu_ms,
|
||||||
measured.readback_cpu_ms, runtimeStats, measured.draw_calls, measured.vertices,
|
measured.readback_cpu_ms, runtimeStats, measured.draw_calls, measured.vertices,
|
||||||
measured.gpu_allocated_bytes, measured.texture_count, debugPhysics});
|
measured.gpu_allocated_bytes, measured.texture_count, debugPhysics,
|
||||||
|
measured.gpu_visibility_active});
|
||||||
}
|
}
|
||||||
++frames;
|
++frames;
|
||||||
}
|
}
|
||||||
@@ -568,6 +601,7 @@ int player_main(int argc, char** argv) {
|
|||||||
{"validation_enabled", stats.validation_enabled},
|
{"validation_enabled", stats.validation_enabled},
|
||||||
{"validation_errors", stats.validation_errors},
|
{"validation_errors", stats.validation_errors},
|
||||||
{"presentation_mode", headless ? "offscreen" : "windowed"},
|
{"presentation_mode", headless ? "offscreen" : "windowed"},
|
||||||
|
{"visibility_mode", visibilityName},
|
||||||
{"simulation_mode", "synthetic_fixed_timestep"},
|
{"simulation_mode", "synthetic_fixed_timestep"},
|
||||||
{"fixed_delta_seconds", config.fixedDelta},
|
{"fixed_delta_seconds", config.fixedDelta},
|
||||||
{"percentile_method", "nearest_rank_all_completed_frames_no_warmup_exclusion"},
|
{"percentile_method", "nearest_rank_all_completed_frames_no_warmup_exclusion"},
|
||||||
@@ -593,6 +627,8 @@ int player_main(int argc, char** argv) {
|
|||||||
{"ticks", completedTicks},
|
{"ticks", completedTicks},
|
||||||
{"dimension", document.value("dimension", 3)},
|
{"dimension", document.value("dimension", 3)},
|
||||||
{"device", stats.device},
|
{"device", stats.device},
|
||||||
|
{"visibility_mode", visibilityName},
|
||||||
|
{"gpu_visibility_active", stats.gpu_visibility_active},
|
||||||
{"validation_errors", stats.validation_errors}}
|
{"validation_errors", stats.validation_errors}}
|
||||||
.dump()
|
.dump()
|
||||||
<< '\n';
|
<< '\n';
|
||||||
|
|||||||
@@ -66,3 +66,23 @@ waits for every frame even without this overlay. Read
|
|||||||
[profiling and measurements](profiling.md) and the
|
[profiling and measurements](profiling.md) and the
|
||||||
[P2 acceptance study](https://github.com/emil28092005/Faset_Engine/blob/main/docs/studies/19-p2-gpu-visibility-acceptance.md)
|
[P2 acceptance study](https://github.com/emil28092005/Faset_Engine/blob/main/docs/studies/19-p2-gpu-visibility-acceptance.md)
|
||||||
for the measured scope and limitations.
|
for the measured scope and limitations.
|
||||||
|
|
||||||
|
## Select a mode in the Player
|
||||||
|
|
||||||
|
The exported Player uses Direct by default. Choose a GPU mode for one run with
|
||||||
|
`--visibility`; the same option works for a development Player and a relocated
|
||||||
|
standalone game:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./faset_player --visibility gpu-frustum
|
||||||
|
./faset_player --visibility gpu-occlusion --headless --frames 120 \
|
||||||
|
--profile visibility-profile.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Accepted values are `direct`, `gpu-frustum`, and `gpu-occlusion`. The profile
|
||||||
|
records the requested `visibility_mode` and each frame's
|
||||||
|
`gpu_visibility_active` status. If the device lacks the GPU path, the Player
|
||||||
|
reports a Direct fallback; selecting a mode alone does not prove that it ran.
|
||||||
|
The Editor's Diagnostics selector changes only its viewport and does not save a
|
||||||
|
Player setting. An exported game contains the P2 shader bundle and does not need
|
||||||
|
the Slang compiler at runtime.
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ struct Snapshot {
|
|||||||
enum class VisibilityMode { Direct, GpuFrustum, GpuOcclusion };
|
enum class VisibilityMode { Direct, GpuFrustum, GpuOcclusion };
|
||||||
// CPU-only validation used before publishing a game or creating Vulkan pipelines.
|
// CPU-only validation used before publishing a game or creating Vulkan pipelines.
|
||||||
void validate_shader_bundle(const std::filesystem::path& directory);
|
void validate_shader_bundle(const std::filesystem::path& directory);
|
||||||
|
void validate_gpu_shader_bundle(const std::filesystem::path& directory);
|
||||||
|
|
||||||
struct RendererConfig {
|
struct RendererConfig {
|
||||||
std::uint32_t width{1280}, height{720};
|
std::uint32_t width{1280}, height{720};
|
||||||
|
|||||||
@@ -216,4 +216,7 @@ detail::load_gpu_shader_bundle(const std::filesystem::path& directory) {
|
|||||||
void validate_shader_bundle(const std::filesystem::path& directory) {
|
void validate_shader_bundle(const std::filesystem::path& directory) {
|
||||||
(void)detail::load_shader_bundle(directory);
|
(void)detail::load_shader_bundle(directory);
|
||||||
}
|
}
|
||||||
|
void validate_gpu_shader_bundle(const std::filesystem::path& directory) {
|
||||||
|
(void)detail::load_gpu_shader_bundle(directory);
|
||||||
|
}
|
||||||
} // namespace faset::render
|
} // namespace faset::render
|
||||||
|
|||||||
@@ -36,4 +36,21 @@ with tempfile.TemporaryDirectory(prefix="faset-player-diagnostics-") as temporar
|
|||||||
assert validated.returncode == 0, validated.stderr
|
assert validated.returncode == 0, validated.stderr
|
||||||
assert "TEST_" not in validated.stderr, validated.stderr
|
assert "TEST_" not in validated.stderr, validated.stderr
|
||||||
assert json.loads(validated.stdout)["validated"]
|
assert json.loads(validated.stdout)["validated"]
|
||||||
|
|
||||||
|
for mode, active in [("direct", False), ("gpu-frustum", True),
|
||||||
|
("gpu-occlusion", True)]:
|
||||||
|
mode_profile = root / f"{mode}.json"
|
||||||
|
selected = subprocess.run([sys.argv[1], "--scene", str(scene), "--headless",
|
||||||
|
"--frames", "2", "--visibility", mode,
|
||||||
|
"--profile", str(mode_profile)],
|
||||||
|
capture_output=True, text=True, encoding="utf-8", timeout=30)
|
||||||
|
assert selected.returncode == 0, (mode, selected.stdout, selected.stderr)
|
||||||
|
mode_report = json.loads(mode_profile.read_text(encoding="utf-8"))
|
||||||
|
assert mode_report["visibility_mode"] == mode, mode_report
|
||||||
|
assert all(sample["gpu_visibility_active"] is active
|
||||||
|
for sample in mode_report["samples"]), mode_report["samples"]
|
||||||
|
|
||||||
|
invalid_mode = subprocess.run([sys.argv[1], "--visibility", "missing"],
|
||||||
|
capture_output=True, text=True, encoding="utf-8", timeout=20)
|
||||||
|
assert invalid_mode.returncode != 0 and "visibility" in invalid_mode.stderr.lower(), invalid_mode
|
||||||
print("Player normal shutdown diagnostics, preserved tick/profile stats and callback-free validation passed")
|
print("Player normal shutdown diagnostics, preserved tick/profile stats and callback-free validation passed")
|
||||||
|
|||||||
Reference in New Issue
Block a user