Devops rework - #43
Conversation
| let mut content = String::new(); | ||
| for line in template.lines() { | ||
| let trimmed = line.trim_start(); | ||
| if trimmed.starts_with("workspace ") || trimmed.starts_with("workspace=") { |
There was a problem hiding this comment.
why do we tolerate 2 options?
| content.push_str("[workspace]\nresolver = \"2\"\n"); | ||
| } else if trimmed.starts_with("pill_engine ") || trimmed.starts_with("pill_engine=") { | ||
| content.push_str(&format!( | ||
| "pill_engine = {{ path = \"{pill_engine}\", features = [\"project\", \"internal\"] }}\n" |
There was a problem hiding this comment.
also, as mentioned above - I don't think project option is a good way ahead
| } | ||
|
|
||
| /// Copy the WASM template into a scratch directory so the engine workspace stays pristine. | ||
| pub(crate) fn prepare_scratch_crate( |
There was a problem hiding this comment.
can we name all WASM-related functions with a wasm_ prefix? This way until we have another usege for them they can be distinguished as wasm-only functions.
|
|
||
| /// Resolve Git-on-Windows pseudo-symlinks (small text files containing a relative path). | ||
| /// Real symlinks and regular files pass through unchanged. | ||
| pub(crate) fn resolve_pseudo_symlink(path: &Path) -> PathBuf { |
There was a problem hiding this comment.
maybe Windows-only helpers should not bloat non-Windows builds? Add platform flag please
| /// Panics if the engine workspace directory cannot be found. | ||
| pub(crate) fn get_path(location: Location) -> PathBuf { | ||
| // engine workspace directory = .../Pill-Engine/engine | ||
| let engine_workspace = |
There was a problem hiding this comment.
this is a quite heavy operation all in all - we will be traversing up to find the desired directory. I think we could cache this somehow so that we don't find it each time. Unless this is called with various different paths from various locations - then it's hard to cache it once for all. I would prefer, though to establish it once and forget about it and use a common relative path everywhere to a known root.
| BuildTarget::Web => { | ||
| let port: u16 = matches | ||
| .value_of("wasm-port") | ||
| .unwrap_or("8080") |
There was a problem hiding this comment.
why 2 level unwrapping here?
| pill_runtime = { path = "../pill_runtime" } | ||
|
|
||
| winit = "0.30.12" | ||
| winit = "0.30" |
| // vec3 fog_color; // offset 0 (12 bytes) | ||
| // float fog_density; // offset 12 (4 bytes) | ||
| // // total: 16 bytes | ||
| // float3 fog_color; // offset 0, size 12, but std140 aligns vec3 to 16 |
There was a problem hiding this comment.
this will soon get deprecated - are these changes necessary?
| # Other | ||
| glam = { version = "0.30.8", features = ["serde", "bytemuck"] } | ||
| winit = "0.30.12" | ||
| glam = { version = "0.33", features = ["serde", "bytemuck"] } |
| console_log = "1" | ||
| log = { version = "0.4", features = ["release_max_level_off"] } | ||
| winit = { version = "0.30.12" } | ||
| winit = { version = "0.30" } |
| "pill_native", | ||
| "pill_web", | ||
| "pill_runtime", | ||
| "NO_PATH", ### Game project crate (This will be changed by Pill Launcher on build to allow proper compilation of game project) |
There was a problem hiding this comment.
glad we got rid of that 🍾 good job!
| ## CI | ||
|
|
||
| A dedicated `benchmark` job runs on every push to `main` and every PR. It uses | ||
| `benchmark_windowed` mode with `xvfb-run` for a virtual display on headless runners. |
There was a problem hiding this comment.
is this mode still valid? Did not see something like that in the commits.
|
|
||
| const BENCHMARK_CITIZEN_COUNT: usize = 10_000; | ||
| const BENCHMARK_DEFAULT_MAX_FRAMES: u64 = 5_000; | ||
| const BENCHMARK_WARMUP_FRAMES: u64 = 1_000; |
There was a problem hiding this comment.
do we need 1000 frames? Usually it's enough to run N times where N is the size necessary for warming up all caches - I encountered up to 100 iterations in other projects, so maybe we don't need that many. Sometimes it's also good to discard the last N frames that introduce outliers - though this can imply some weird platform-specific hoodoo.
I link here a good resource on this topic for posterity https://gernot-heiser.org/benchmarking-crimes.html
| .build(), | ||
| )?; | ||
| let _orange = engine.add_resource::<Material>( | ||
| Material::builder(shared::ORANGE_MATERIAL_NAME) |
| shared::PLANE_MESH_NAME, | ||
| shared::PLANE_MESH_PATH.into(), | ||
| ))?; | ||
| let _pill = engine.add_resource(Mesh::new( |
| #[cfg(feature = "benchmark_windowed")] | ||
| let (orange_material, pill_mesh) = { | ||
| let material = engine.get_resource_handle::<Material>("orange")?; | ||
| let mesh = engine.get_resource_handle::<Mesh>("pill")?; |
There was a problem hiding this comment.
ah, pill's material and mesh are created only for non-headless builds?, then maybe gate them behind headless?
| // -- Statistics -------------------------------------------------------------- | ||
|
|
||
| fn print_report(state: &BenchmarkState) { | ||
| let times = &state.frame_times_ms; |
There was a problem hiding this comment.
| @@ -0,0 +1,266 @@ | |||
| //! This file implements the headless runner, activated by the `headless` feature. | |||
| //! | |||
| //! Replaces the winit-based run_app() with a bare update loop — no window, | |||
There was a problem hiding this comment.
vernacular is a bit LLMish - "3 times 'no'" then "fully supported" -how can it be partially supported :D
| .join("pill_launcher") | ||
| .join("target") | ||
| .join("debug") | ||
| .join("PillLauncherUpstream"), |
There was a problem hiding this comment.
is this standardized somewhere? I did not see it anywhere else yet.
| // initialise a fresh engine inside the new runtime. | ||
| let project_dylib_path = | ||
| CString::new(project_path_for_create.to_string_lossy().as_bytes())?; | ||
| let args = if let Some(ctx) = runtime_context { |
There was a problem hiding this comment.
| let args = if let Some(ctx) = runtime_context { | |
| let args = if let Some(context) = runtime_context { |
| let config_path = project_resources_directory_path.join("config.ini"); | ||
|
|
||
| // --- 3. Determine runtime load mode (dylib vs in-process) --- | ||
| // Decide how to load the runtime (dynamic library or in-process). |
There was a problem hiding this comment.
| // Decide how to load the runtime (dynamic library or in-process). |
| fn run_app() -> Result<()> { | ||
| // --- 1. Detect project directory, run layout, and hot-reload mode --- | ||
| // In the development build, standalone will look for the resource files in the "res" directory of the pill project directory | ||
| // In the release build, "res" directory is copied to /build/release/data/res (TODO: pack all resources use by pill project into a single data file) |
There was a problem hiding this comment.
Is this task tracked somewhere?
| let runtime_load_mode = parse_runtime_load_mode(std::env::var("PILL_RUNTIME_MODE").ok()) | ||
| .or(in_process.then_some(RuntimeLoadMode::InProcess)) | ||
| .unwrap_or(if cfg!(target_os = "macos") { | ||
| RuntimeLoadMode::InProcess |
There was a problem hiding this comment.
This hints that macOs does dynamic library loading differently? Am I correct?
|
|
||
| /// Returns true when the engine workspace's Cargo.toml lists the given | ||
| /// project directory name as a workspace member. | ||
| fn workspace_includes_project( |
There was a problem hiding this comment.
While I like having a bulletproof system - do we need all these shenaningans? Sounds a bit like - try X,Y,Z and mash it with a hammer until it works. Shouldn't be have an elegant solution and expect our devs to apply it? We also can allow for "do whatever the heck you want" but this comes at much higher maintenance cost.
| engine_source_directory_path: Option<&Path>, | ||
| name: &str, | ||
| ) -> Vec<PathBuf> { | ||
| let mut candidates = vec![build_data_directory_path.join(paths::dylib(name))]; |
There was a problem hiding this comment.
again, quite expensive operations that might slow down hot-reloading significantly.
| if trimmed.starts_with("members") && trimmed.contains('[') { | ||
| in_members = true; | ||
| } | ||
| if in_members && !inserted && trimmed == "]" { |
There was a problem hiding this comment.
this code is duplicated, maybe a further commonalization pass could be handy here? I noticed several other such similar/duplicated functions - could you take a closer look and eliminate those?
|
|
||
| /// Build project + pill_native + pill_runtime via cargo in the engine workspace. | ||
| /// Copies the standalone executable and dynamic libraries into the output directory. | ||
| /// Supports --features, hot-reload, PlantUML pre-rendering, --headless, and per-project target dirs. |
There was a problem hiding this comment.
duplicated verbose comment with the one above
| cargo_command.env("CARGO_TERM_COLOR", "always"); | ||
| } | ||
|
|
||
| let mut cargo_child = if use_experimental_logs_parser() { |
There was a problem hiding this comment.
I don't get the reason for using an experimental log parser
There was a problem hiding this comment.
Is this because we want to parse the logs for failures for CI?
JDuchniewicz
left a comment
There was a problem hiding this comment.
Some minor changes and cleanups necessary. Otherwise looks very solid and well-thought through. Good job 👍🏽
Seeing the end of the big-refactor tunnel (pill_tunnel) already 💊

Comprehensive overhaul of the Pill project infrastructure, launcher architecture, and developer tooling.
Pill Launcher Refactoring
The launcher's monolithic
main.rs(~1,400 lines) has been split into a trait-based action dispatch architecture:actions/-build,run,create,docs,cargo,assets,link,unlinkutils/-cli,common,files,paths,native_target,wasm_target,web_dev_server,workspace,assets,plantumlActiontrait and registers its own CLI flags with clap--helpCI Pipeline Overhaul
The old monolithic
ci.ymlhas been replaced with 4 focused GitHub Actions workflows:ci-basic-tests.ymlci-build-image.ymlci-examples-tests.ymlci-pill_launcher-tests.ymlThe actual test logic lives in 3 bash test scripts with shared infrastructure (
common.sh):run_basic_tests.shrun_examples_tests.shrun_pill_launcher_tests.shbash devops/tests/run_basic_tests.shghcr.io/mattszymonski/pill-ci:latest(Alpine + Rust + wasm-pack + slangc + plantuml)New: Build Size & Performance Benchmarks
The CI pipeline now includes automated tracking of binary sizes and frame-time performance on every push:
build/release/data/; WASM builds report the.wasmbinary. Size regressions (e.g.pill_runtime.dlljumping from 10 MB to 15 MB) are immediately visible in CI logs.examples/city(10,000 citizens, 5,000 frames) is built and run 3× on every push. Per-frame timing statistics (average_ms,median_ms,min_ms,max_ms,range_ms,stddev_ms) are extracted from the game's JSON output and aggregated across runs. Supports both windowed and headless modes with automatic fallback..wasmbinary is checked against a hard budget of 0.499 MiB (523,239 bytes). Exceeding it fails the CI check, preventing accidental binary bloat from new dependencies or missing optimizations.Examples & Templates
examples/city- 10,000 citizens, 5,000 frames, JSON frame-time output, supports windowed & headless modespill_default→new_projectgame.rs→project.rsacross all examples - projects no longer uselib.rs, onlyproject.rsis neededexamples/Empty(consolidated)Cargo.tomlfiles updated for new template pathsNaming: Game → Project
The term "game" has been renamed to "project" throughout the codebase, reflecting that Pill supports more than just games.
Documentation added: