diff --git a/build/dependencies.lock b/build/dependencies.lock index 73695c296..8f4190c63 100644 --- a/build/dependencies.lock +++ b/build/dependencies.lock @@ -207,10 +207,10 @@ legacy_channels = ["development"] [legacy_inventory] manifest = "build/legacy-manifest.json" schema = 1 -component_count = 335 -file_count = 2134 -byte_count = 314704844 -source_build_components = 231 +component_count = 336 +file_count = 2135 +byte_count = 314711917 +source_build_components = 232 unresolved_components = 104 inherited_baseline = "07505ea58c7bba698d6b9220ff43946a43cac76b" diff --git a/build/legacy-manifest.json b/build/legacy-manifest.json index 6c5114b6c..69b4567b3 100644 --- a/build/legacy-manifest.json +++ b/build/legacy-manifest.json @@ -488,8 +488,8 @@ "kind": "runtime", "path": "static/build/.tmp_update/bin/bloom-dev-deploy", "file_count": 1, - "byte_count": 32904, - "tree_sha256": "ac0661051a48261c2a585f1dc21ce172c61ed19edb1b2a182660f0202950bc9d", + "byte_count": 33011, + "tree_sha256": "4dd16ce6d73174d683d2b6b43161024a1694de4b48c58842ce89e80b15fa394e", "source": "https://github.com/boburning/BloomOS", "source_revision": "release-commit", "license": "GPL-3.0-only", @@ -522,6 +522,19 @@ "build_recipe": "Makefile", "resolution": "source-build" }, + { + "id": "runtime-tmp-update-bin-bloom-display-session-prepare", + "kind": "runtime", + "path": "static/build/.tmp_update/bin/bloom-display-session-prepare", + "file_count": 1, + "byte_count": 3190, + "tree_sha256": "5c4dff01fab14bac6d77ae7d0109d04e6b6eb736038a7d145f8d62db57b8a9bc", + "source": "https://github.com/boburning/BloomOS", + "source_revision": "release-commit", + "license": "GPL-3.0-only", + "build_recipe": "Makefile", + "resolution": "source-build" + }, { "id": "runtime-tmp-update-bin-bloom-game-smoke", "kind": "runtime", @@ -748,8 +761,8 @@ "kind": "runtime", "path": "static/build/.tmp_update/bin/bloom-ra-session-prepare", "file_count": 1, - "byte_count": 12860, - "tree_sha256": "07f87ee2a7b1e88155bf0ff0d4087fdf7861f3dcb9bf02cce3276191a4434de7", + "byte_count": 12798, + "tree_sha256": "dd8a2a3ac3222a7dacf5e0a4546c6107b738f24f420101aaac658e5c841ef471", "source": "https://github.com/boburning/BloomOS", "source_revision": "release-commit", "license": "GPL-3.0-only", @@ -813,8 +826,8 @@ "kind": "runtime", "path": "static/build/.tmp_update/bin/bloom-session", "file_count": 1, - "byte_count": 25092, - "tree_sha256": "be2f99f54505a7e9081a0603775383cbbc5072f8da1d6063fdf9ef7f2d504f56", + "byte_count": 28930, + "tree_sha256": "3f7d914d334cd99468f9c7713f58c594ba26685f37d0e4117d47dd1255deca24", "source": "https://github.com/boburning/BloomOS", "source_revision": "release-commit", "license": "GPL-3.0-only", diff --git a/docs/1.0_TEST_MATRIX.md b/docs/1.0_TEST_MATRIX.md index 8f6b6c56d..ba28ce545 100644 --- a/docs/1.0_TEST_MATRIX.md +++ b/docs/1.0_TEST_MATRIX.md @@ -22,6 +22,7 @@ observable properties and never substitutes for physical UX review. | Parsers | Theme, JSON, paths, images, and schema migration fuzz/property tests | | Display Auto compatibility | Deterministic per-system/core effective visual fixture plus explicit no-selectable-core catalog dispositions; exact policy/catalog/manifest, core binary, core metadata, global, core-override, and content-directory hashes; representative legacy geometry/effect assertions; runtime user and per-game overrides excluded | | Display profile semantics | Exact Scaling, Screen Effect, and Bezel controls; disjoint geometry/sampling/effect/artwork key ownership; PLAT-01 framebuffer capability; bounded centered Auto/integer/aspect/fill geometry fixtures for 640x480 and 752x560; invalid geometry and stale/coupled/unsafe policy mutations fail without partial config | +| Display settings and session composer | Canonical backward-compatible display selections; exact enum rejection; no Onion projection; observed framebuffer input; independent axis fallback; one private `0600` RetroArch append config shared with achievements; no permanent-config mutation; decorator failure and timeout preserve the immutable base launch | | Performance | Input/render timings, 30-minute shell soak, 500 open/back cycles | | Privacy | Allowlisted health/support export contains no private data | | Developer deployment | Dual-marker/update/session preflight; deterministic allowlisted manifest; exact byte/hash/mode transfer; sealed slots; atomic activation; readiness auto-rollback; active/previous overlay; bounded cleanup; ordinary network-helper activation denied | diff --git a/docs/DISPLAY_SESSION_COMPOSER.md b/docs/DISPLAY_SESSION_COMPOSER.md new file mode 100644 index 000000000..9b59aa1a5 --- /dev/null +++ b/docs/DISPLAY_SESSION_COMPOSER.md @@ -0,0 +1,49 @@ +# Display settings and session composer + +DISP-03 stores display selections in the schema-1 `bloom-settings` authority and +applies them only through Bloom's private RetroArch session configuration. It +never edits the global RetroArch configuration or an inherited core/content +override. + +The canonical `display` object contains four independent selections: + +- `scaling`: `auto`, `integer-pixels`, `correct-aspect`, or `fill`; +- `sampling`: `auto`, `nearest`, or `linear`; +- `screen_effect`: `none`, `auto`, `lcd`, `crt`, or `custom`; +- `bezel`: `none`, `auto`, or `custom`. + +Older schema-1 settings without this object read as four `auto` values. The +first display mutation creates the complete object atomically. Display values +are not projected into Onion compatibility state. + +At session start, `bloom-display-session-prepare` reads those typed values and +the observed `bloom-platform` framebuffer dimensions. It asks `bloom-launch` +to contribute display-owned keys to the existing trusted composer. The +resulting file is a regular private `0600` file under `/tmp/bloom-session/`. +The session transaction validates and attaches exactly that one file. If +RetroAchievements is enabled, its credential and safety contributions import +the validated display contribution and atomically replace the same file. + +`auto` does not rewrite an axis from the DISP-01 baseline. `fill`, nearest or +linear sampling, effect `none`, and bezel `none` have complete typed composer +implementations. Integer Pixels and Correct Aspect remain unavailable until a +trusted source raster/aspect is connected. LCD, CRT, and custom effect or +bezel selections remain unavailable until DISP-04/DISP-05 provide reviewed +identities. An unavailable selection resolves that axis to `auto`, records a +bounded reason, and emits no partial keys for that axis. + +The display owner can emit only the allowlisted display keys in +`bloom_retroarch_config.c`; it cannot emit achievement, input-safety, driver, +network, or arbitrary filesystem settings. Custom selections are bounded IDs +in settings and never become raw paths in DISP-03. + +Validation: + +```sh +make test GTEST_FILTER='BloomSettingsTest.*:BloomRetroarchConfigTest.*:BloomLaunchTest.*' +docker run --rm -v "$PWD:/workspace" -w /workspace \ + ghcr.io/boburning/bloomos-shell-test@sha256:36adeca7c265345688ebf47b575c56e350b9016257e0a9ed15c63dc7e0447d63 \ + test/shell/display_session_prepare.bats \ + test/shell/bloom_session.bats \ + test/shell/ra_session_prepare.bats +``` diff --git a/docs/validation/display/disp03-20260831/shell-restored.png b/docs/validation/display/disp03-20260831/shell-restored.png new file mode 100644 index 000000000..a42778101 Binary files /dev/null and b/docs/validation/display/disp03-20260831/shell-restored.png differ diff --git a/docs/validation/display/plus-disp03-settings-composer-20260831.json b/docs/validation/display/plus-disp03-settings-composer-20260831.json new file mode 100644 index 000000000..c63b4c521 --- /dev/null +++ b/docs/validation/display/plus-disp03-settings-composer-20260831.json @@ -0,0 +1,117 @@ +{ + "schema": 1, + "date": "2026-08-31", + "program_slice": "DISP-03", + "pull_request": 608, + "operator": "Codex over maintainer-authorized key-only SSH", + "source_commit": "6050dde80e9b821fd527ef2498ab1a63f1e4d67e", + "base_commit": "5967607dad0067cefb9ba73feb209a2c702b77d6", + "host_validation": { + "focused_native": "64/64 passed", + "full_native": "593/593 passed in 16.11 seconds", + "focused_shell_and_deployment": "115/115 passed", + "full_shell": "792/792 passed across eight shards", + "static_analysis": "focused cppcheck exited 0", + "required_ci": [ + "Check build", + "format", + "shell-test", + "Static analysis", + "Test build", + "AddressSanitizer" + ], + "required_ci_result": "all passed" + }, + "signed_build": { + "workflow_run": "https://github.com/boburning/BloomOS/actions/runs/33345813017", + "version": "0.1.0-dev-6050dde8", + "archive_size": 199862348, + "archive_sha256": "66904930f2c076ab122b4af26b8010403c178002519106afdeaa147089d7da40", + "manifest_sha256": "ad387c51dd5bd393e314c59a272420236dcb3b6bd8fe426abd59735eeaa69e2f", + "manifest_signature_sha256": "3f3ac4f304643033ae0333861cdd44c2e71ce78c81eaf2d2f34c34254f4bdb87", + "payload_manifest_sha256": "4e8309164c62d33c30b6afc6f4d80b18e200695706699a34fbe5067d9e596849", + "build_info_sha256": "c6270dd3c93428f917a3ab9ffe5d999bc7b8f14f9c32fe161b3165726248c389", + "sha256sums_verified": true, + "ed25519_signature_verified": true, + "installed_as_full_update": false + }, + "device": { + "model": "miyoo-mini-plus", + "model_id": "354", + "display": "640x480", + "installed_baseline": "v0.1.0-dev-2933f706", + "developer_ssh": "running_key_only", + "physical_certification": false + }, + "deployment": { + "tier": "hash-differential Warm overlay", + "candidate_id": "disp03-launch-6050dde8", + "candidate_component": "launch-session", + "candidate_manifest_files": 11, + "candidate_manifest_exact_hashes_verified_before_and_after_activation": true, + "temporary_deployer_size": 33011, + "temporary_deployer_sha256": "c3cbc050c733ed93d60fce93417daa495b6fc83b7912624fdbee062f9c46adaf", + "temporary_deployer_removed": true, + "full_update_performed": false, + "network_component_deployed": false + }, + "device_gate": { + "preflight_ready": true, + "session_state_before_and_after": "IDLE", + "retroarch_process_started": false, + "legacy_schema_without_display": { + "status": "applied", + "effective_scaling": "auto", + "effective_screen_effect": "auto", + "effective_bezel": "auto", + "private_config_mode": "0600", + "private_config": [ + "video_fullscreen = \"true\"" + ] + }, + "fill_nearest_none_none": { + "status": "applied", + "observed_panel": "640x480", + "private_config_mode": "0600", + "geometry": "bounded full-panel viewport", + "sampling": "nearest", + "effect": "none", + "bezel": "none" + }, + "global_retroarch_config_sha256_before": "336ee748ca06bba32582087efc867f811f465558df158056b9a25689bf36672e", + "global_retroarch_config_sha256_after": "336ee748ca06bba32582087efc867f811f465558df158056b9a25689bf36672e", + "canonical_settings_sha256_before": "31fe5ad0969bc8a9e831d10b77752da4712b30d0b96082c8243a6fa495316c9d", + "canonical_settings_sha256_after": "31fe5ad0969bc8a9e831d10b77752da4712b30d0b96082c8243a6fa495316c9d", + "wifi_state_before_during_and_after": "enabled and associated", + "task_temporary_configs_removed": true, + "candidate_helper_removed_on_rollback": true + }, + "preliminary_fail_closed_checks": [ + "a non-canonical synthetic GameID was rejected before config composition", + "a config path outside /tmp/bloom-session was rejected before file creation", + "an idle-session directory lifecycle race failed before file creation; the final gate recreated the canonical private directory after confirming IDLE" + ], + "restoration": { + "candidate_rolled_back": true, + "current_shell_overlay": "disp03-shell-restore-6050dde8", + "current_shell_manifest_exact_hashes_verified": true, + "final_preflight_ready": true, + "final_wifi_state": "enabled and associated", + "final_retroarch_processes": 0 + }, + "screenshots": { + "shell_restored": { + "path": "docs/validation/display/disp03-20260831/shell-restored.png", + "size": 17029, + "sha256": "26f66de76bfad9d7a04c25b60168c410726e103e1aa34c1e3f1ae342e341d495", + "privacy_safe": true + } + }, + "limitations": [ + "This is remote SSH, process, config, and framebuffer evidence; it is not physical controls, LCD quality, audio, or gameplay certification.", + "The gate validated composer output without launching a ROM or touching Saves or user data.", + "This evidence certifies the Miyoo Mini Plus only, not Mini V1-V4 or Mini Flip/my285.", + "Integer Pixels and Correct Aspect remain Auto until trusted source raster/aspect is connected.", + "LCD, CRT, and custom effect or bezel selections remain Auto until DISP-04 and DISP-05 provide reviewed identities." + ] +} diff --git a/src/bloomLaunch/bloom_launch.c b/src/bloomLaunch/bloom_launch.c index 6f90f7948..d26b87f85 100644 --- a/src/bloomLaunch/bloom_launch.c +++ b/src/bloomLaunch/bloom_launch.c @@ -968,6 +968,80 @@ int bloom_launch_copy_achievement_state(const char *source_path, const char *des cJSON_Delete(destination); return result; } + +int bloom_launch_write_display_config(const char *config_path, const char *scaling, + const char *sampling, const char *screen_effect, + const char *bezel, unsigned panel_width, + unsigned panel_height, char *effective_scaling, + size_t effective_scaling_size, char *effective_effect, + size_t effective_effect_size, char *effective_bezel, + size_t effective_bezel_size, char *reason, + size_t reason_size, char *error, size_t error_size) +{ + if (!path_under(config_path, "/tmp/bloom-session/")) { + set_error(error, error_size, "display config path is invalid"); + return -1; + } + BloomRetroarchConfig *composer = bloom_retroarch_config_create(); + char config[4096]; + size_t length = 0; + int composed = composer == NULL ? -1 : bloom_retroarch_config_apply_display(composer, scaling, sampling, screen_effect, bezel, panel_width, panel_height, effective_scaling, effective_scaling_size, effective_effect, effective_effect_size, effective_bezel, effective_bezel_size, reason, reason_size, error, error_size); + if (composer == NULL) + set_error(error, error_size, "cannot allocate RetroArch config composer"); + if (composed == 0) + composed = bloom_retroarch_config_render(composer, config, sizeof(config), &length, + error, error_size); + bloom_retroarch_config_destroy(composer); + if (composed != 0 || atomic_write_bytes(config_path, config, length, 0600, error, error_size) != 0 || + bloom_retroarch_config_validate_file(config_path, error, error_size) != 0) { + memset(config, 0, sizeof(config)); + unlink(config_path); + return -1; + } + memset(config, 0, sizeof(config)); + return 0; +} + +int bloom_launch_attach_session_config(const char *request_path, const char *config_path, + char *error, size_t error_size) +{ + if (!path_under(config_path, "/tmp/bloom-session/")) { + set_error(error, error_size, "session config path is invalid"); + return -1; + } + cJSON *request = NULL; + if (load_valid(request_path, &request, error, error_size) != 0) + return -1; + int schema = request_schema(request); + int attached = 0; + if (schema == 1) { + cJSON *configs = cJSON_GetObjectItemCaseSensitive(request, "append_configs"); + if (cJSON_GetArraySize(configs) == 0) + attached = cJSON_AddItemToArray(configs, cJSON_CreateString(config_path)); + else if (cJSON_GetArraySize(configs) == 1) { + cJSON *current = cJSON_GetArrayItem(configs, 0); + attached = cJSON_IsString(current) && strcmp(current->valuestring, config_path) == 0; + } + } + else { + cJSON *current = cJSON_GetObjectItemCaseSensitive(request, "append_config"); + if (cJSON_IsNull(current)) + attached = cJSON_ReplaceItemInObjectCaseSensitive(request, "append_config", + cJSON_CreateString(config_path)); + else + attached = cJSON_IsString(current) && strcmp(current->valuestring, config_path) == 0; + } + if (!attached || !valid_request(request, error, error_size)) { + cJSON_Delete(request); + if (error != NULL && error[0] == '\0') + set_error(error, error_size, "session config could not be attached"); + return -1; + } + int result = write_request_json(request_path, request, error, error_size); + cJSON_Delete(request); + return result; +} + int bloom_launch_write_ra_config(const char *request_path, const char *config_path, const char *username, const char *token, const char *proxy_host, char *error, size_t error_size) { @@ -999,6 +1073,20 @@ int bloom_launch_write_ra_config(const char *request_path, const char *config_pa int composed = composer != NULL ? 0 : -1; if (composer == NULL) set_error(error, error_size, "cannot allocate RetroArch config composer"); + if (composed == 0 && access(config_path, F_OK) == 0) { + char import_error[256] = {0}; + if (bloom_retroarch_config_import_file(composer, config_path, import_error, + sizeof(import_error)) != 0) { + bloom_retroarch_config_destroy(composer); + composer = bloom_retroarch_config_create(); + if (composer == NULL) { + memset(config, 0, sizeof(config)); + cJSON_Delete(request); + set_error(error, error_size, "cannot allocate RetroArch config composer"); + return -1; + } + } + } if (composed == 0) composed = bloom_retroarch_config_set_bool(composer, BLOOM_RA_CONFIG_ACHIEVEMENTS, "cheevos_enable", 1, error, error_size); diff --git a/src/bloomLaunch/bloom_launch.h b/src/bloomLaunch/bloom_launch.h index 5fb98653a..976332f0f 100644 --- a/src/bloomLaunch/bloom_launch.h +++ b/src/bloomLaunch/bloom_launch.h @@ -34,5 +34,15 @@ int bloom_launch_resolve_achievement_transport(const char *request_path, int ena const char *core_certification, char *error, size_t error_size); int bloom_launch_write_ra_config(const char *request_path, const char *config_path, const char *username, const char *token, const char *proxy_host, char *error, size_t error_size); +int bloom_launch_write_display_config(const char *config_path, const char *scaling, + const char *sampling, const char *screen_effect, + const char *bezel, unsigned panel_width, + unsigned panel_height, char *effective_scaling, + size_t effective_scaling_size, char *effective_effect, + size_t effective_effect_size, char *effective_bezel, + size_t effective_bezel_size, char *reason, + size_t reason_size, char *error, size_t error_size); +int bloom_launch_attach_session_config(const char *request_path, const char *config_path, + char *error, size_t error_size); #endif diff --git a/src/bloomLaunch/bloom_retroarch_config.c b/src/bloomLaunch/bloom_retroarch_config.c index 1aa1a99d8..349ad88fb 100644 --- a/src/bloomLaunch/bloom_retroarch_config.c +++ b/src/bloomLaunch/bloom_retroarch_config.c @@ -15,6 +15,7 @@ typedef enum { VALUE_BOOL, VALUE_UNSIGNED, VALUE_STRING, + VALUE_STRING_OR_EMPTY, VALUE_NULL_BINDING, } ValueType; @@ -45,6 +46,27 @@ static const KeySpec KEY_SPECS[] = { {"input_cheat_index_plus", BLOOM_RA_CONFIG_SESSION_SAFETY, VALUE_NULL_BINDING, 0}, {"input_cheat_index_minus", BLOOM_RA_CONFIG_SESSION_SAFETY, VALUE_NULL_BINDING, 0}, {"input_cheat_toggle", BLOOM_RA_CONFIG_SESSION_SAFETY, VALUE_NULL_BINDING, 0}, + {"video_fullscreen", BLOOM_RA_CONFIG_DISPLAY, VALUE_BOOL, 0}, + {"aspect_ratio_index", BLOOM_RA_CONFIG_DISPLAY, VALUE_UNSIGNED, 23}, + {"custom_viewport_height", BLOOM_RA_CONFIG_DISPLAY, VALUE_UNSIGNED, 4096}, + {"custom_viewport_width", BLOOM_RA_CONFIG_DISPLAY, VALUE_UNSIGNED, 4096}, + {"custom_viewport_x", BLOOM_RA_CONFIG_DISPLAY, VALUE_UNSIGNED, 4096}, + {"custom_viewport_y", BLOOM_RA_CONFIG_DISPLAY, VALUE_UNSIGNED, 4096}, + {"video_aspect_ratio_auto", BLOOM_RA_CONFIG_DISPLAY, VALUE_BOOL, 0}, + {"video_crop_overscan", BLOOM_RA_CONFIG_DISPLAY, VALUE_BOOL, 0}, + {"video_dingux_ipu_keep_aspect", BLOOM_RA_CONFIG_DISPLAY, VALUE_BOOL, 0}, + {"video_force_aspect", BLOOM_RA_CONFIG_DISPLAY, VALUE_BOOL, 0}, + {"video_fullscreen_x", BLOOM_RA_CONFIG_DISPLAY, VALUE_UNSIGNED, 4096}, + {"video_fullscreen_y", BLOOM_RA_CONFIG_DISPLAY, VALUE_UNSIGNED, 4096}, + {"video_rotation", BLOOM_RA_CONFIG_DISPLAY, VALUE_UNSIGNED, 3}, + {"video_scale_integer", BLOOM_RA_CONFIG_DISPLAY, VALUE_BOOL, 0}, + {"video_scale_integer_overscale", BLOOM_RA_CONFIG_DISPLAY, VALUE_BOOL, 0}, + {"video_dingux_ipu_filter_type", BLOOM_RA_CONFIG_DISPLAY, VALUE_UNSIGNED, 1}, + {"video_smooth", BLOOM_RA_CONFIG_DISPLAY, VALUE_BOOL, 0}, + {"video_filter", BLOOM_RA_CONFIG_DISPLAY, VALUE_STRING_OR_EMPTY, 0}, + {"video_shader_enable", BLOOM_RA_CONFIG_DISPLAY, VALUE_BOOL, 0}, + {"input_overlay", BLOOM_RA_CONFIG_DISPLAY, VALUE_STRING_OR_EMPTY, 0}, + {"input_overlay_enable", BLOOM_RA_CONFIG_DISPLAY, VALUE_BOOL, 0}, }; typedef struct { @@ -86,6 +108,11 @@ static bool safe_string(const char *value) return true; } +static bool safe_string_or_empty(const char *value) +{ + return value != NULL && (value[0] == '\0' || safe_string(value)); +} + static int set_value(BloomRetroarchConfig *config, BloomRetroarchConfigOwner owner, const char *key, ValueType type, const char *value, char *error, size_t error_size) @@ -158,8 +185,10 @@ int bloom_retroarch_config_set_string(BloomRetroarchConfig *config, int index = spec_index(key); if (index < 0 || (KEY_SPECS[index].type != VALUE_STRING && + KEY_SPECS[index].type != VALUE_STRING_OR_EMPTY && KEY_SPECS[index].type != VALUE_NULL_BINDING) || - !safe_string(value) || + (KEY_SPECS[index].type == VALUE_STRING_OR_EMPTY ? !safe_string_or_empty(value) + : !safe_string(value)) || (KEY_SPECS[index].type == VALUE_NULL_BINDING && strcmp(value, "nul") != 0)) { set_error(error, error_size, "RetroArch string config value is invalid"); return -1; @@ -167,6 +196,120 @@ int bloom_retroarch_config_set_string(BloomRetroarchConfig *config, return set_value(config, owner, key, KEY_SPECS[index].type, value, error, error_size); } +static bool selection(const char *value, const char *const *allowed, size_t count) +{ + if (value == NULL) + return false; + for (size_t index = 0; index < count; ++index) + if (strcmp(value, allowed[index]) == 0) + return true; + return false; +} + +int bloom_retroarch_config_apply_display(BloomRetroarchConfig *config, + const char *scaling, const char *sampling, + const char *screen_effect, const char *bezel, + unsigned panel_width, unsigned panel_height, + char *effective_scaling, size_t effective_scaling_size, + char *effective_effect, size_t effective_effect_size, + char *effective_bezel, size_t effective_bezel_size, + char *reason, size_t reason_size, + char *error, size_t error_size) +{ + static const char *const scaling_values[] = {"auto", "integer-pixels", "correct-aspect", "fill"}; + static const char *const sampling_values[] = {"auto", "nearest", "linear"}; + static const char *const effect_values[] = {"none", "auto", "lcd", "crt", "custom"}; + static const char *const bezel_values[] = {"none", "auto", "custom"}; + if (config == NULL || effective_scaling == NULL || effective_scaling_size < 24 || + effective_effect == NULL || effective_effect_size < 16 || effective_bezel == NULL || + effective_bezel_size < 16 || reason == NULL || reason_size < 48 || + !selection(scaling, scaling_values, sizeof(scaling_values) / sizeof(scaling_values[0])) || + !selection(sampling, sampling_values, sizeof(sampling_values) / sizeof(sampling_values[0])) || + !selection(screen_effect, effect_values, sizeof(effect_values) / sizeof(effect_values[0])) || + !selection(bezel, bezel_values, sizeof(bezel_values) / sizeof(bezel_values[0])) || + panel_width > 4096 || panel_height > 4096) { + set_error(error, error_size, "display config input is invalid"); + return -1; + } + snprintf(effective_scaling, effective_scaling_size, "%s", scaling); + snprintf(effective_effect, effective_effect_size, "%s", screen_effect); + snprintf(effective_bezel, effective_bezel_size, "%s", bezel); + reason[0] = '\0'; + if ((strcmp(scaling, "integer-pixels") == 0 || strcmp(scaling, "correct-aspect") == 0)) { + snprintf(effective_scaling, effective_scaling_size, "auto"); + snprintf(reason, reason_size, "source_geometry_unavailable"); + } + else if (strcmp(scaling, "fill") == 0 && (panel_width == 0 || panel_height == 0)) { + snprintf(effective_scaling, effective_scaling_size, "auto"); + snprintf(reason, reason_size, "framebuffer_unavailable"); + } + if (strcmp(screen_effect, "lcd") == 0 || strcmp(screen_effect, "crt") == 0 || + strcmp(screen_effect, "custom") == 0) { + snprintf(effective_effect, effective_effect_size, "auto"); + if (reason[0] == '\0') + snprintf(reason, reason_size, "effect_profile_unavailable"); + } + if (strcmp(bezel, "custom") == 0) { + snprintf(effective_bezel, effective_bezel_size, "auto"); + if (reason[0] == '\0') + snprintf(reason, reason_size, "bezel_profile_unavailable"); + } + + int result = bloom_retroarch_config_set_bool(config, BLOOM_RA_CONFIG_DISPLAY, + "video_fullscreen", 1, error, error_size); +#define DISPLAY_BOOL(key, value) \ + do { \ + if (result == 0) \ + result = bloom_retroarch_config_set_bool(config, BLOOM_RA_CONFIG_DISPLAY, key, value, error, error_size); \ + } while (0) +#define DISPLAY_UINT(key, value) \ + do { \ + if (result == 0) \ + result = bloom_retroarch_config_set_unsigned(config, BLOOM_RA_CONFIG_DISPLAY, key, value, error, error_size); \ + } while (0) +#define DISPLAY_STRING(key, value) \ + do { \ + if (result == 0) \ + result = bloom_retroarch_config_set_string(config, BLOOM_RA_CONFIG_DISPLAY, key, value, error, error_size); \ + } while (0) + if (strcmp(effective_scaling, "fill") == 0) { + DISPLAY_UINT("aspect_ratio_index", 23); + DISPLAY_UINT("custom_viewport_height", panel_height); + DISPLAY_UINT("custom_viewport_width", panel_width); + DISPLAY_UINT("custom_viewport_x", 0); + DISPLAY_UINT("custom_viewport_y", 0); + DISPLAY_BOOL("video_aspect_ratio_auto", 0); + DISPLAY_BOOL("video_crop_overscan", 0); + DISPLAY_BOOL("video_dingux_ipu_keep_aspect", 0); + DISPLAY_BOOL("video_force_aspect", 1); + DISPLAY_UINT("video_fullscreen_x", panel_width); + DISPLAY_UINT("video_fullscreen_y", panel_height); + DISPLAY_UINT("video_rotation", 0); + DISPLAY_BOOL("video_scale_integer", 0); + DISPLAY_BOOL("video_scale_integer_overscale", 0); + } + if (strcmp(sampling, "nearest") == 0) { + DISPLAY_UINT("video_dingux_ipu_filter_type", 0); + DISPLAY_BOOL("video_smooth", 0); + } + else if (strcmp(sampling, "linear") == 0) { + DISPLAY_UINT("video_dingux_ipu_filter_type", 1); + DISPLAY_BOOL("video_smooth", 1); + } + if (strcmp(effective_effect, "none") == 0) { + DISPLAY_STRING("video_filter", ""); + DISPLAY_BOOL("video_shader_enable", 0); + } + if (strcmp(effective_bezel, "none") == 0) { + DISPLAY_STRING("input_overlay", ""); + DISPLAY_BOOL("input_overlay_enable", 0); + } +#undef DISPLAY_BOOL +#undef DISPLAY_UINT +#undef DISPLAY_STRING + return result; +} + int bloom_retroarch_config_render(const BloomRetroarchConfig *config, char *output, size_t output_size, size_t *written, char *error, size_t error_size) @@ -197,6 +340,8 @@ int bloom_retroarch_config_render(const BloomRetroarchConfig *config, char *outp static bool valid_rendered_value(const KeySpec *spec, const char *value) { + if (spec->type == VALUE_STRING_OR_EMPTY) + return safe_string_or_empty(value); if (!safe_string(value)) return false; if (spec->type == VALUE_BOOL) @@ -292,3 +437,54 @@ int bloom_retroarch_config_validate_file(const char *path, char *error, memset(bytes, 0, sizeof(bytes)); return result; } + +int bloom_retroarch_config_import_file(BloomRetroarchConfig *config, const char *path, + char *error, size_t error_size) +{ + if (config == NULL || bloom_retroarch_config_validate_file(path, error, error_size) != 0) + return -1; + int descriptor = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + struct stat metadata; + if (descriptor < 0 || fstat(descriptor, &metadata) != 0 || metadata.st_size <= 0 || + metadata.st_size > 8192) { + if (descriptor >= 0) + close(descriptor); + set_error(error, error_size, "cannot import RetroArch config"); + return -1; + } + char bytes[8193]; + size_t length = (size_t)metadata.st_size; + ssize_t count = read(descriptor, bytes, length); + int close_result = close(descriptor); + if (count != (ssize_t)length || close_result != 0) { + memset(bytes, 0, sizeof(bytes)); + set_error(error, error_size, "cannot import complete RetroArch config"); + return -1; + } + bytes[length] = '\0'; + int result = 0; + for (char *cursor = bytes; result == 0 && *cursor != '\0';) { + char *newline = strchr(cursor, '\n'); + char *separator = strstr(cursor, " = \""); + if (newline == NULL || separator == NULL) { + result = -1; + break; + } + *newline = '\0'; + size_t line_length = strlen(cursor); + cursor[line_length - 1] = '\0'; + *separator = '\0'; + const char *value = separator + 4; + int index = spec_index(cursor); + if (index < 0) + result = -1; + else + result = set_value(config, KEY_SPECS[index].owner, KEY_SPECS[index].key, + KEY_SPECS[index].type, value, error, error_size); + cursor = newline + 1; + } + memset(bytes, 0, sizeof(bytes)); + if (result != 0 && error != NULL && error[0] == '\0') + set_error(error, error_size, "cannot import RetroArch config"); + return result; +} diff --git a/src/bloomLaunch/bloom_retroarch_config.h b/src/bloomLaunch/bloom_retroarch_config.h index 64d1eeaee..ef92e908f 100644 --- a/src/bloomLaunch/bloom_retroarch_config.h +++ b/src/bloomLaunch/bloom_retroarch_config.h @@ -25,6 +25,17 @@ int bloom_retroarch_config_set_string(BloomRetroarchConfig *config, BloomRetroarchConfigOwner owner, const char *key, const char *value, char *error, size_t error_size); +int bloom_retroarch_config_apply_display(BloomRetroarchConfig *config, + const char *scaling, const char *sampling, + const char *screen_effect, const char *bezel, + unsigned panel_width, unsigned panel_height, + char *effective_scaling, size_t effective_scaling_size, + char *effective_effect, size_t effective_effect_size, + char *effective_bezel, size_t effective_bezel_size, + char *reason, size_t reason_size, + char *error, size_t error_size); +int bloom_retroarch_config_import_file(BloomRetroarchConfig *config, const char *path, + char *error, size_t error_size); int bloom_retroarch_config_render(const BloomRetroarchConfig *config, char *output, size_t output_size, size_t *written, char *error, size_t error_size); diff --git a/src/bloomLaunch/main.c b/src/bloomLaunch/main.c index 02e1accdc..08340d8ce 100644 --- a/src/bloomLaunch/main.c +++ b/src/bloomLaunch/main.c @@ -14,6 +14,8 @@ static void usage(void) " bloom-launch resolve-achievements REQUEST.json ENABLED MODE OFFLINE_CASUAL PROXY_READY RA_GAME_ID CERTIFICATION\n" " bloom-launch set-achievements-state REQUEST.json ENABLED REQUESTED EFFECTIVE TRANSPORT REASON|- RA_GAME_ID CERTIFICATION\n" " bloom-launch copy-achievements-state SOURCE.json DESTINATION.json\n" + " bloom-launch write-display-config CONFIG SCALING SAMPLING EFFECT BEZEL WIDTH HEIGHT\n" + " bloom-launch attach-config REQUEST.json CONFIG\n" " bloom-launch write-ra-config REQUEST.json CONFIG USERNAME PROXY_HOST|- < TOKEN\n" " bloom-launch write-legacy REQUEST.json COMMAND.sh\n"); } @@ -72,6 +74,34 @@ int main(int argc, char **argv) } else if (argc == 4 && strcmp(argv[1], "copy-achievements-state") == 0) result = bloom_launch_copy_achievement_state(argv[2], argv[3], error, sizeof(error)); + else if (argc == 4 && strcmp(argv[1], "attach-config") == 0) + result = bloom_launch_attach_session_config(argv[2], argv[3], error, sizeof(error)); + else if (argc == 9 && strcmp(argv[1], "write-display-config") == 0) { + char *width_end = NULL; + char *height_end = NULL; + unsigned long width = strtoul(argv[7], &width_end, 10); + unsigned long height = strtoul(argv[8], &height_end, 10); + char effective_scaling[24] = {}; + char effective_effect[16] = {}; + char effective_bezel[16] = {}; + char reason[48] = {}; + if (width_end == argv[7] || *width_end != '\0' || height_end == argv[8] || + *height_end != '\0' || width > 4096 || height > 4096) { + usage(); + return 2; + } + result = bloom_launch_write_display_config( + argv[2], argv[3], argv[4], argv[5], argv[6], (unsigned)width, (unsigned)height, + effective_scaling, sizeof(effective_scaling), effective_effect, + sizeof(effective_effect), effective_bezel, sizeof(effective_bezel), reason, + sizeof(reason), error, sizeof(error)); + if (result == 0) + printf("{\"schema\":1,\"status\":\"%s\",\"effective\":{\"scaling\":\"%s\"," + "\"screen_effect\":\"%s\",\"bezel\":\"%s\"},\"reason\":%s%s%s}\n", + reason[0] == '\0' ? "applied" : "degraded", effective_scaling, + effective_effect, effective_bezel, reason[0] == '\0' ? "null" : "\"", + reason, reason[0] == '\0' ? "" : "\""); + } else if (argc == 6 && strcmp(argv[1], "write-ra-config") == 0) { char token[128] = {0}; if (fgets(token, sizeof(token), stdin) == NULL || (!feof(stdin) && strchr(token, '\n') == NULL)) { diff --git a/src/bloomSettings/bloom_settings.c b/src/bloomSettings/bloom_settings.c index 60e4468e8..304a0ca55 100644 --- a/src/bloomSettings/bloom_settings.c +++ b/src/bloomSettings/bloom_settings.c @@ -245,11 +245,13 @@ static cJSON *build_settings(const cJSON *system, const cJSON *keymap, const cha cJSON *interface = cJSON_CreateObject(); cJSON *behavior = cJSON_CreateObject(); cJSON *controls = cJSON_CreateObject(); + cJSON *display = cJSON_CreateObject(); cJSON *blue_light = cJSON_CreateObject(); cJSON *recording = cJSON_CreateObject(); cJSON *compatibility = cJSON_CreateObject(); if (root == NULL || source == NULL || device == NULL || network == NULL || interface == NULL || behavior == NULL || - controls == NULL || blue_light == NULL || recording == NULL || compatibility == NULL) + controls == NULL || display == NULL || blue_light == NULL || recording == NULL || + compatibility == NULL) goto failure; cJSON_AddNumberToObject(root, "schema", BLOOM_SETTINGS_SCHEMA); @@ -367,6 +369,13 @@ static cJSON *build_settings(const cJSON *system, const cJSON *keymap, const cha cJSON_AddItemToObject(root, "controls", controls); controls = NULL; + cJSON_AddStringToObject(display, "scaling", "auto"); + cJSON_AddStringToObject(display, "sampling", "auto"); + cJSON_AddStringToObject(display, "screen_effect", "auto"); + cJSON_AddStringToObject(display, "bezel", "auto"); + cJSON_AddItemToObject(root, "display", display); + display = NULL; + cJSON_AddItemToObject(compatibility, "onion_system", cJSON_Duplicate(system, 1)); cJSON_AddItemToObject(compatibility, "onion_keymap", cJSON_Duplicate(keymap, 1)); cJSON_AddItemToObject(root, "compatibility", compatibility); @@ -381,6 +390,7 @@ static cJSON *build_settings(const cJSON *system, const cJSON *keymap, const cha cJSON_Delete(interface); cJSON_Delete(behavior); cJSON_Delete(controls); + cJSON_Delete(display); cJSON_Delete(blue_light); cJSON_Delete(recording); cJSON_Delete(compatibility); @@ -772,6 +782,57 @@ static int optional_theme_variant(const cJSON *interface, char *output, size_t s return 1; } +static int value_in_set(const char *value, const char *const *allowed, size_t count) +{ + if (value == NULL) + return 0; + for (size_t index = 0; index < count; ++index) + if (strcmp(value, allowed[index]) == 0) + return 1; + return 0; +} + +static int valid_display_selection(const char *field, const char *value) +{ + static const char *const scaling[] = {"auto", "integer-pixels", "correct-aspect", "fill"}; + static const char *const sampling[] = {"auto", "nearest", "linear"}; + static const char *const effect[] = {"none", "auto", "lcd", "crt", "custom"}; + static const char *const bezel[] = {"none", "auto", "custom"}; + if (strcmp(field, "display.scaling") == 0) + return value_in_set(value, scaling, sizeof(scaling) / sizeof(scaling[0])); + if (strcmp(field, "display.sampling") == 0) + return value_in_set(value, sampling, sizeof(sampling) / sizeof(sampling[0])); + if (strcmp(field, "display.screen_effect") == 0) + return value_in_set(value, effect, sizeof(effect) / sizeof(effect[0])); + if (strcmp(field, "display.bezel") == 0) + return value_in_set(value, bezel, sizeof(bezel) / sizeof(bezel[0])); + return 0; +} + +static int optional_display_values(const cJSON *display, BloomSettingsValues *values) +{ + if (display == NULL) { + snprintf(values->display_scaling, sizeof(values->display_scaling), "auto"); + snprintf(values->display_sampling, sizeof(values->display_sampling), "auto"); + snprintf(values->display_screen_effect, sizeof(values->display_screen_effect), "auto"); + snprintf(values->display_bezel, sizeof(values->display_bezel), "auto"); + return 1; + } + return cJSON_IsObject(display) && + required_string(display, "scaling", values->display_scaling, + sizeof(values->display_scaling), 0) && + required_string(display, "sampling", values->display_sampling, + sizeof(values->display_sampling), 0) && + required_string(display, "screen_effect", values->display_screen_effect, + sizeof(values->display_screen_effect), 0) && + required_string(display, "bezel", values->display_bezel, + sizeof(values->display_bezel), 0) && + valid_display_selection("display.scaling", values->display_scaling) && + valid_display_selection("display.sampling", values->display_sampling) && + valid_display_selection("display.screen_effect", values->display_screen_effect) && + valid_display_selection("display.bezel", values->display_bezel); +} + static int parse_settings_values(const cJSON *root, BloomSettingsValues *values, char *error, size_t error_size) { @@ -782,6 +843,7 @@ static int parse_settings_values(const cJSON *root, BloomSettingsValues *values, const cJSON *interface = cJSON_GetObjectItemCaseSensitive(root, "interface"); const cJSON *behavior = cJSON_GetObjectItemCaseSensitive(root, "behavior"); const cJSON *controls = cJSON_GetObjectItemCaseSensitive(root, "controls"); + const cJSON *display = cJSON_GetObjectItemCaseSensitive(root, "display"); const cJSON *blue_light = cJSON_IsObject(interface) ? cJSON_GetObjectItemCaseSensitive(interface, "blue_light") : NULL; @@ -793,6 +855,7 @@ static int parse_settings_values(const cJSON *root, BloomSettingsValues *values, values->authority, sizeof(values->authority), error, error_size) != 0 || !cJSON_IsObject(controls) || !cJSON_IsObject(blue_light) || !cJSON_IsObject(recording) || + !optional_display_values(display, values) || !required_integer(device, "volume", 0, 20, &values->volume) || !required_boolean(device, "mute", &values->mute) || !required_integer(device, "background_music_volume", 0, 20, @@ -1547,7 +1610,11 @@ static const BloomSettingsFieldPolicy SETTINGS_FIELDS[] = { STRING_FIELD("controls.mainui_button_x", "controls", NULL, "mainui_button_x", BLOOM_SETTINGS_TEXT_MAX, 1), STRING_FIELD("controls.mainui_button_y", "controls", NULL, "mainui_button_y", - BLOOM_SETTINGS_TEXT_MAX, 1)}; + BLOOM_SETTINGS_TEXT_MAX, 1), + STRING_FIELD("display.scaling", "display", NULL, "scaling", 24, 0), + STRING_FIELD("display.sampling", "display", NULL, "sampling", 16, 0), + STRING_FIELD("display.screen_effect", "display", NULL, "screen_effect", 16, 0), + STRING_FIELD("display.bezel", "display", NULL, "bezel", 16, 0)}; static const BloomSettingsFieldPolicy *find_field_policy(const char *field) { @@ -1621,6 +1688,10 @@ int bloom_settings_set(const char *settings_path, const char *onion_system_path, set_error(error, error_size, "settings theme variant is invalid"); return -1; } + if (strncmp(field, "display.", 8) == 0 && !valid_display_selection(field, value)) { + set_error(error, error_size, "settings display selection is invalid"); + return -1; + } cJSON *replacement = create_field_value(policy, value, error, error_size); if (replacement == NULL) return -1; @@ -1650,6 +1721,18 @@ int bloom_settings_set(const char *settings_path, const char *onion_system_path, goto failure; } } + if (target == NULL && strcmp(policy->section, "display") == 0) { + target = cJSON_CreateObject(); + if (target == NULL || !cJSON_AddStringToObject(target, "scaling", "auto") || + !cJSON_AddStringToObject(target, "sampling", "auto") || + !cJSON_AddStringToObject(target, "screen_effect", "auto") || + !cJSON_AddStringToObject(target, "bezel", "auto") || + !cJSON_AddItemToObject(root, "display", target)) { + cJSON_Delete(target); + set_error(error, error_size, "canonical display settings could not be created"); + goto failure; + } + } if (policy->group != NULL) target = cJSON_IsObject(target) ? cJSON_GetObjectItemCaseSensitive(target, policy->group) : NULL; diff --git a/src/bloomSettings/bloom_settings.h b/src/bloomSettings/bloom_settings.h index 2f76900a9..e631380b8 100644 --- a/src/bloomSettings/bloom_settings.h +++ b/src/bloomSettings/bloom_settings.h @@ -95,6 +95,10 @@ typedef struct { int ingame_double_press; char mainui_button_x[BLOOM_SETTINGS_TEXT_MAX]; char mainui_button_y[BLOOM_SETTINGS_TEXT_MAX]; + char display_scaling[24]; + char display_sampling[16]; + char display_screen_effect[16]; + char display_bezel[16]; } BloomSettingsValues; typedef struct { diff --git a/src/bloomSettings/main.c b/src/bloomSettings/main.c index aca38dd20..ec7bd4d73 100644 --- a/src/bloomSettings/main.c +++ b/src/bloomSettings/main.c @@ -128,13 +128,15 @@ int main(int argc, char **argv) "\"authority\":\"%s\",\"device\":{\"brightness\":%d,\"volume\":%d," "\"mute\":%s,\"wifi_enabled\":%s},\"network\":{\"ssh_enabled\":%s," "\"sftp_enabled\":%s,\"samba_enabled\":%s},\"interface\":{\"theme\":%s," - "\"theme_variant\":%s,\"font_size\":%d}}\n", + "\"theme_variant\":%s,\"font_size\":%d},\"display\":{\"scaling\":\"%s\"," + "\"sampling\":\"%s\",\"screen_effect\":\"%s\",\"bezel\":\"%s\"}}\n", values.generation, values.authority, values.brightness, values.volume, values.mute ? "true" : "false", values.wifi_enabled ? "true" : "false", values.network_ssh_enabled ? "true" : "false", values.network_sftp_enabled ? "true" : "false", values.network_samba_enabled ? "true" : "false", - theme_json, variant_json, values.font_size); + theme_json, variant_json, values.font_size, values.display_scaling, + values.display_sampling, values.display_screen_effect, values.display_bezel); cJSON_free(theme_json); cJSON_free(variant_json); return 0; diff --git a/static/build/.tmp_update/bin/bloom-dev-deploy b/static/build/.tmp_update/bin/bloom-dev-deploy index f81960d46..be05089f8 100755 --- a/static/build/.tmp_update/bin/bloom-dev-deploy +++ b/static/build/.tmp_update/bin/bloom-dev-deploy @@ -78,7 +78,8 @@ allowed_path() { launch-session:bin/bloom-shell|launch-session:bin/gameSwitcher|\ launch-session:bin/keymon|launch-session:bin/bloom-ra|\ launch-session:bin/bloom-launch|launch-session:bin/bloom-launch-run|\ - launch-session:bin/bloom-launch-override|launch-session:bin/bloom-session|\ + launch-session:bin/bloom-launch-override|launch-session:bin/bloom-display-session-prepare|\ + launch-session:bin/bloom-session|\ launch-session:bin/bloom-ra-session-prepare|\ launch-session:script/bloom-launch-trace.sh) return 0 ;; network-helpers:bin/bloom-settings|network-helpers:bin/bloom-network|\ @@ -181,6 +182,7 @@ validate_manifest() { bin/bloom-launch \ bin/bloom-launch-override \ bin/bloom-launch-run \ + bin/bloom-display-session-prepare \ bin/bloom-ra \ bin/bloom-ra-session-prepare \ bin/bloom-session \ diff --git a/static/build/.tmp_update/bin/bloom-display-session-prepare b/static/build/.tmp_update/bin/bloom-display-session-prepare new file mode 100755 index 000000000..0ae53e04b --- /dev/null +++ b/static/build/.tmp_update/bin/bloom-display-session-prepare @@ -0,0 +1,67 @@ +#!/bin/sh +set -eu +umask 077 + +LAUNCH_BIN=${BLOOM_LAUNCH_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-launch} +SETTINGS_BIN=${BLOOM_SETTINGS_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-settings} +PLATFORM_BIN=${BLOOM_PLATFORM_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-platform} +JQ_BIN=${BLOOM_JQ_BIN:-/mnt/SDCARD/.tmp_update/bin/jq} +SESSION_ROOT=${BLOOM_SESSION_ROOT:-/tmp/bloom-session} + +fail() { printf 'bloom-display-session-prepare: %s\n' "$*" >&2; exit 1; } + +[ "$#" -eq 1 ] || fail "expected one launch request" +request=$1 +[ -x "$LAUNCH_BIN" ] && [ -x "$SETTINGS_BIN" ] && [ -x "$JQ_BIN" ] || + fail "display session services are unavailable" +"$LAUNCH_BIN" validate "$request" >/dev/null || fail "launch request is invalid" +emulator_type=$("$LAUNCH_BIN" get "$request" emulator_type 2>/dev/null) || + fail "launch backend is unavailable" +[ "$emulator_type" = retroarch ] || fail "display config requires RetroArch" + +settings_json=$("$SETTINGS_BIN" values 2>/dev/null) || fail "canonical settings are unavailable" +display_json=$(printf '%s\n' "$settings_json" | "$JQ_BIN" -cer ' + select(.schema == 1 and .service == "bloom-settings") | + if has("display") then + select( + (.display | type) == "object" and + (.display.scaling | type) == "string" and + (.display.sampling | type) == "string" and + (.display.screen_effect | type) == "string" and + (.display.bezel | type) == "string" + ) | .display + else + {scaling:"auto", sampling:"auto", screen_effect:"auto", bezel:"auto"} + end') || fail "canonical display settings are invalid" +scaling=$(printf '%s\n' "$display_json" | "$JQ_BIN" -er '.scaling') || + fail "display scaling selection is unavailable" +sampling=$(printf '%s\n' "$display_json" | "$JQ_BIN" -er '.sampling') || + fail "display sampling selection is unavailable" +screen_effect=$(printf '%s\n' "$display_json" | "$JQ_BIN" -er '.screen_effect') || + fail "display effect selection is unavailable" +bezel=$(printf '%s\n' "$display_json" | "$JQ_BIN" -er '.bezel') || + fail "display bezel selection is unavailable" + +panel_width=0 +panel_height=0 +if [ -x "$PLATFORM_BIN" ]; then + panel_width=$("$PLATFORM_BIN" display-width 2>/dev/null || printf '0') + panel_height=$("$PLATFORM_BIN" display-height 2>/dev/null || printf '0') +fi +case "$panel_width" in ''|*[!0-9]*) panel_width=0 ;; esac +case "$panel_height" in ''|*[!0-9]*) panel_height=0 ;; esac +[ "$panel_width" -le 4096 ] 2>/dev/null || panel_width=0 +[ "$panel_height" -le 4096 ] 2>/dev/null || panel_height=0 + +rm -f "$SESSION_ROOT/ra.cfg" +result=$("$LAUNCH_BIN" write-display-config "$SESSION_ROOT/ra.cfg" "$scaling" "$sampling" \ + "$screen_effect" "$bezel" "$panel_width" "$panel_height") || + fail "display config could not be composed" +printf '%s\n' "$result" | "$JQ_BIN" -e \ + '.schema == 1 and (.status == "applied" or .status == "degraded") and + (.effective.scaling | type == "string") and + (.effective.screen_effect | type == "string") and + (.effective.bezel | type == "string") and + (.reason == null or (.reason | type == "string"))' >/dev/null 2>&1 || + fail "display result is invalid" +printf '%s\n' "$result" diff --git a/static/build/.tmp_update/bin/bloom-ra-session-prepare b/static/build/.tmp_update/bin/bloom-ra-session-prepare index 36f204c4e..ca01ee5cd 100755 --- a/static/build/.tmp_update/bin/bloom-ra-session-prepare +++ b/static/build/.tmp_update/bin/bloom-ra-session-prepare @@ -74,7 +74,6 @@ disable_ra() { esac "$LAUNCH_BIN" set-achievements-state "$request" false "${requested_mode:-disabled}" disabled unavailable "$reason" 0 not_applicable || fail_integrity "achievement policy could not be disabled" - rm -f "$SESSION_ROOT/ra.cfg" [ ! -x "$RA_LOG" ] || "$RA_LOG" prepare-degraded "$reason" >/dev/null 2>&1 || true printf '{"schema":1,"enabled":false,"mode":"disabled","transport":"unavailable","reason":"%s"}\n' "$reason" } @@ -280,7 +279,6 @@ fi fail_integrity "achievement policy could not be resolved" bloom_launch_trace_mark ra_prepare_policy_written [ -f "$CREDENTIALS" ] && [ ! -L "$CREDENTIALS" ] || { disable_ra authentication_required; exit 0; } -rm -f "$SESSION_ROOT/ra.cfg" "$LAUNCH_BIN" write-ra-config "$request" "$SESSION_ROOT/ra.cfg" "$username" "$proxy_host" <"$CREDENTIALS" || { disable_ra config_generation_failed; exit 0; } bloom_launch_trace_mark ra_prepare_config_written diff --git a/static/build/.tmp_update/bin/bloom-session b/static/build/.tmp_update/bin/bloom-session index 13650dbb4..8ac487cfa 100755 --- a/static/build/.tmp_update/bin/bloom-session +++ b/static/build/.tmp_update/bin/bloom-session @@ -7,6 +7,7 @@ PROC_ROOT="${BLOOM_PROC_ROOT:-/proc}" UPTIME_FILE="${BLOOM_UPTIME_FILE:-$PROC_ROOT/uptime}" BLOOM_LAUNCH="${BLOOM_LAUNCH_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-launch}" BLOOM_RA_PREPARE="${BLOOM_RA_PREPARE_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-ra-session-prepare}" +BLOOM_DISPLAY_PREPARE="${BLOOM_DISPLAY_PREPARE_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-display-session-prepare}" BLOOM_RA_LOG="${BLOOM_RA_LOG_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-ra-log}" BLOOM_SAVE_FLUSH="${BLOOM_SAVE_FLUSH_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-save-flush}" BLOOM_LIBRARY="${BLOOM_LIBRARY_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-library}" @@ -67,22 +68,69 @@ cleanup_candidate_artifacts() { decorator_status() { ra_status="$1" ra_reason="$2" + case "$DISPLAY_DECORATOR_STATUS:$DISPLAY_DECORATOR_REASON" in + not_requested:not_applicable|applied:none|degraded:helper_failed|degraded:timeout|degraded:malformed_output|degraded:source_geometry_unavailable|degraded:framebuffer_unavailable|degraded:effect_profile_unavailable|degraded:bezel_profile_unavailable) ;; + *) fail "display decorator status is invalid" ;; + esac case "$ra_status:$ra_reason" in not_requested:not_applicable|applied:none|degraded:helper_failed|degraded:timeout|degraded:malformed_output) ;; *) fail "decorator status is invalid" ;; esac temporary="$DECORATOR_STATUS_FILE.tmp.$$" - (umask 077; printf '{"schema":1,"display":{"status":"not_requested","reason":"not_applicable"},"compatibility":{"status":"not_requested","reason":"not_applicable"},"retroachievements":{"status":"%s","reason":"%s"}}\n' \ - "$ra_status" "$ra_reason" >"$temporary") || fail "cannot write decorator status" + (umask 077; printf '{"schema":1,"display":{"status":"%s","reason":"%s"},"compatibility":{"status":"not_requested","reason":"not_applicable"},"retroachievements":{"status":"%s","reason":"%s"}}\n' \ + "$DISPLAY_DECORATOR_STATUS" "$DISPLAY_DECORATOR_REASON" "$ra_status" "$ra_reason" >"$temporary") || fail "cannot write decorator status" mv -f "$temporary" "$DECORATOR_STATUS_FILE" || fail "cannot commit decorator status" } +run_display_decorator() { + work_file="$ARTIFACT_ROOT/display/request.json" + result_file="$ARTIFACT_ROOT/display/result.json" + mkdir -p "$ARTIFACT_ROOT/display" || return 1 + chmod 700 "$ARTIFACT_ROOT" "$ARTIFACT_ROOT/display" 2>/dev/null || true + (umask 077; cp "$CANDIDATE_FILE" "$work_file") || return 1 + bloom_launch_trace_mark session_display_decorator_begin + BLOOM_SESSION_ROOT="$ARTIFACT_ROOT/display" "$BLOOM_DISPLAY_PREPARE" "$work_file" >"$result_file" 2>/dev/null & + decorator_pid=$! + elapsed=0 + while kill -0 "$decorator_pid" 2>/dev/null && [ "$elapsed" -lt "$DECORATOR_TIMEOUT" ]; do + sleep 1 + elapsed=$((elapsed + 1)) + done + if kill -0 "$decorator_pid" 2>/dev/null; then + kill -TERM "$decorator_pid" 2>/dev/null || true + wait "$decorator_pid" 2>/dev/null || true + return 124 + fi + wait "$decorator_pid" + decorator_result=$? + [ "$decorator_result" -eq 0 ] || return "$decorator_result" + [ -f "$ARTIFACT_ROOT/display/ra.cfg" ] && [ ! -L "$ARTIFACT_ROOT/display/ra.cfg" ] || return 3 + "$JQ_BIN" -e '.schema == 1 and (.status == "applied" or .status == "degraded") and + (.effective.scaling | type == "string") and (.effective.screen_effect | type == "string") and + (.effective.bezel | type == "string") and (.reason == null or (.reason | type == "string"))' \ + "$result_file" >/dev/null 2>&1 || return 3 + mv -f "$ARTIFACT_ROOT/display/ra.cfg" "$SESSION_ROOT/ra.cfg" || return 2 + display_result_status=$("$JQ_BIN" -er '.status' "$result_file") || return 3 + if [ "$display_result_status" = degraded ]; then + DISPLAY_DECORATOR_STATUS=degraded + DISPLAY_DECORATOR_REASON=$("$JQ_BIN" -er '.reason | select(. == "source_geometry_unavailable" or . == "framebuffer_unavailable" or . == "effect_profile_unavailable" or . == "bezel_profile_unavailable")' "$result_file") || return 3 + else + DISPLAY_DECORATOR_STATUS=applied + DISPLAY_DECORATOR_REASON=none + fi + bloom_launch_trace_mark session_display_decorator_applied + return 0 +} + run_ra_decorator() { work_file="$ARTIFACT_ROOT/ra/request.json" result_file="$ARTIFACT_ROOT/ra/result.json" mkdir -p "$ARTIFACT_ROOT/ra" || return 1 chmod 700 "$ARTIFACT_ROOT" "$ARTIFACT_ROOT/ra" 2>/dev/null || true (umask 077; cp "$CANDIDATE_FILE" "$work_file") || return 1 + if [ -f "$SESSION_ROOT/ra.cfg" ] && [ ! -L "$SESSION_ROOT/ra.cfg" ]; then + (umask 077; cp "$SESSION_ROOT/ra.cfg" "$ARTIFACT_ROOT/ra/ra.cfg") || return 1 + fi bloom_launch_trace_mark session_ra_decorator_begin BLOOM_SESSION_ROOT="$ARTIFACT_ROOT/ra" "$BLOOM_RA_PREPARE" "$work_file" >"$result_file" 2>/dev/null & decorator_pid=$! @@ -118,6 +166,7 @@ run_ra_decorator() { if [ -f "$ARTIFACT_ROOT/ra/ra.cfg" ]; then [ ! -L "$ARTIFACT_ROOT/ra/ra.cfg" ] || return 2 mv -f "$ARTIFACT_ROOT/ra/ra.cfg" "$SESSION_ROOT/ra.cfg" || return 2 + "$BLOOM_LAUNCH" attach-config "$CANDIDATE_FILE" "$SESSION_ROOT/ra.cfg" >/dev/null || return 2 fi bloom_launch_trace_mark session_ra_decorator_applied return 0 @@ -257,7 +306,28 @@ command_start() { esac bloom_launch_trace_mark session_request_copied bloom_launch_trace_mark session_candidate_ready - bloom_launch_trace_mark session_display_decorator_not_requested + DISPLAY_DECORATOR_STATUS=not_requested + DISPLAY_DECORATOR_REASON=not_applicable + if [ "$emulator_type" = retroarch ] && [ -x "$BLOOM_DISPLAY_PREPARE" ]; then + run_display_decorator + display_prepare_status=$? + if [ "$display_prepare_status" -eq 2 ]; then + cleanup_candidate_artifacts + rm -f "$CANDIDATE_FILE" "$SESSION_ROOT/ra.cfg" + fail "display launch integrity check failed" + elif [ "$display_prepare_status" -ne 0 ]; then + rm -f "$SESSION_ROOT/ra.cfg" + DISPLAY_DECORATOR_STATUS=degraded + if [ "$display_prepare_status" -eq 124 ]; then + DISPLAY_DECORATOR_REASON=timeout + elif [ "$display_prepare_status" -eq 3 ]; then + DISPLAY_DECORATOR_REASON=malformed_output + else + DISPLAY_DECORATOR_REASON=helper_failed + fi + bloom_launch_trace_mark session_display_decorator_discarded + fi + fi bloom_launch_trace_mark session_compatibility_decorator_not_requested if [ "$emulator_type" = retroarch ] && [ -x "$BLOOM_RA_PREPARE" ]; then run_ra_decorator @@ -267,11 +337,6 @@ command_start() { rm -f "$CANDIDATE_FILE" "$SESSION_ROOT/ra.cfg" fail "RetroAchievements launch integrity check failed" elif [ "$ra_prepare_status" -ne 0 ]; then - rm -f "$CANDIDATE_FILE" "$SESSION_ROOT/ra.cfg" - if ! (umask 077; cp "$BASE_FILE" "$CANDIDATE_FILE"); then - cleanup_candidate_artifacts - fail "cannot reset candidate launch request" - fi bloom_launch_trace_mark session_decorator_discarded if [ "$ra_prepare_status" -eq 124 ]; then decorator_status degraded timeout @@ -287,6 +352,11 @@ command_start() { else decorator_status not_requested not_applicable fi + if [ -f "$SESSION_ROOT/ra.cfg" ]; then + [ ! -L "$SESSION_ROOT/ra.cfg" ] || fail "session config is unsafe" + "$BLOOM_LAUNCH" attach-config "$CANDIDATE_FILE" "$SESSION_ROOT/ra.cfg" >/dev/null || + fail "session config could not be attached" + fi bloom_launch_trace_mark session_ra_prepared bloom_launch_trace_mark session_final_validation_begin "$BLOOM_LAUNCH" validate "$CANDIDATE_FILE" >/dev/null || { diff --git a/test/shell/bloom_device.bats b/test/shell/bloom_device.bats index 9032d9d8b..381b116ec 100644 --- a/test/shell/bloom_device.bats +++ b/test/shell/bloom_device.bats @@ -311,6 +311,7 @@ PY bin/bloom-launch \ bin/bloom-launch-override \ bin/bloom-launch-run \ + bin/bloom-display-session-prepare \ bin/bloom-ra \ bin/bloom-ra-session-prepare \ bin/bloom-session \ @@ -327,7 +328,7 @@ PY run "$DEVICE_TOOL" dev-stage test-plus launch-session "$payload" launch-bundle-1 [ "$status" -eq 0 ] - [ "$(jq '.files | length' "$BLOOM_TEST_ROOT/received-manifest.json")" -eq 10 ] + [ "$(jq '.files | length' "$BLOOM_TEST_ROOT/received-manifest.json")" -eq 11 ] rm "$payload/bin/keymon" : >"$MOCK_LOG" diff --git a/test/shell/bloom_session.bats b/test/shell/bloom_session.bats index 83c2d8046..0545109fd 100644 --- a/test/shell/bloom_session.bats +++ b/test/shell/bloom_session.bats @@ -53,6 +53,9 @@ case "$1" in [ "$3" = mgba_libretro.so ] || exit 1 printf '%s\n' mgba-core >>"$2" ;; + attach-config) + printf '%s\n' config-attached >>"$2" + ;; resolve-achievements) printf 'resolve:%s\n' "$*" >>"$MOCK_LOG" ;; *) exit 1 ;; esac @@ -244,6 +247,24 @@ EOF [ ! -e "$BLOOM_SESSION_ROOT/candidate-artifacts" ] } +@test "display decorator commits one private config before optional RA decoration" { + export BLOOM_DISPLAY_PREPARE_BIN="$MOCK_BIN/bloom-display-session-prepare" + cat >"$BLOOM_DISPLAY_PREPARE_BIN" <<'EOF' +#!/bin/sh +printf '%s\n' 'video_fullscreen = "true"' >"$BLOOM_SESSION_ROOT/ra.cfg" +chmod 600 "$BLOOM_SESSION_ROOT/ra.cfg" +printf '%s\n' '{"schema":1,"status":"applied","effective":{"scaling":"auto","screen_effect":"auto","bezel":"auto"},"reason":null}' +EOF + chmod +x "$BLOOM_DISPLAY_PREPARE_BIN" + + "$SESSION" start "$REQUEST" + + grep -Fx 'video_fullscreen = "true"' "$BLOOM_SESSION_ROOT/ra.cfg" + grep -F config-attached "$BLOOM_SESSION_ROOT/request.json" + grep -F '"display":{"status":"applied","reason":"none"}' "$BLOOM_SESSION_ROOT/decorator_status.json" + [ ! -e "$BLOOM_SESSION_ROOT/candidate-artifacts" ] +} + @test "RA decorator commits a verified Auto core change with achievement state" { export BLOOM_RA_PREPARE_BIN="$MOCK_BIN/bloom-ra-session-prepare" export MOCK_COPY_ACHIEVEMENTS_ONLY=1 diff --git a/test/shell/dev_deploy.bats b/test/shell/dev_deploy.bats index 7d70f70a6..fbfc885c0 100644 --- a/test/shell/dev_deploy.bats +++ b/test/shell/dev_deploy.bats @@ -59,6 +59,7 @@ make_launch_manifest() { bin/bloom-launch \ bin/bloom-launch-override \ bin/bloom-launch-run \ + bin/bloom-display-session-prepare \ bin/bloom-ra \ bin/bloom-ra-session-prepare \ bin/bloom-session \ @@ -103,6 +104,7 @@ stage_launch_bundle() { bin/bloom-launch \ bin/bloom-launch-override \ bin/bloom-launch-run \ + bin/bloom-display-session-prepare \ bin/bloom-ra \ bin/bloom-ra-session-prepare \ bin/bloom-session \ diff --git a/test/shell/display_session_prepare.bats b/test/shell/display_session_prepare.bats new file mode 100644 index 000000000..a7b517cb0 --- /dev/null +++ b/test/shell/display_session_prepare.bats @@ -0,0 +1,84 @@ +load 'support/test_helper' + +setup() { + setup_bloom_fixture + export PREPARE=/workspace/static/build/.tmp_update/bin/bloom-display-session-prepare + export BLOOM_SESSION_ROOT="$BLOOM_TEST_ROOT/session" + export BLOOM_LAUNCH_BIN="$MOCK_BIN/bloom-launch" + export BLOOM_SETTINGS_BIN="$MOCK_BIN/bloom-settings" + export BLOOM_PLATFORM_BIN="$MOCK_BIN/bloom-platform" + export BLOOM_JQ_BIN="$(command -v jq)" + mkdir -p "$BLOOM_SESSION_ROOT" + export REQUEST="$BLOOM_TEST_ROOT/request.json" + printf '%s\n' '{"schema":2}' >"$REQUEST" + cat >"$BLOOM_LAUNCH_BIN" <<'EOF' +#!/bin/sh +case "$1" in + validate) [ -f "$2" ] ;; + get) [ "$3" = emulator_type ] && printf '%s\n' retroarch ;; + write-display-config) + printf '%s\n' "$*" >>"$MOCK_LOG" + printf '%s\n' 'video_fullscreen = "true"' >"$2" + chmod 600 "$2" + printf '%s\n' '{"schema":1,"status":"applied","effective":{"scaling":"fill","screen_effect":"none","bezel":"none"},"reason":null}' + ;; + *) exit 1 ;; +esac +EOF + chmod +x "$BLOOM_LAUNCH_BIN" + cat >"$BLOOM_SETTINGS_BIN" <<'EOF' +#!/bin/sh +[ "$1" = values ] || exit 1 +printf '%s\n' '{"schema":1,"service":"bloom-settings","display":{"scaling":"fill","sampling":"nearest","screen_effect":"none","bezel":"none"}}' +EOF + chmod +x "$BLOOM_SETTINGS_BIN" + cat >"$BLOOM_PLATFORM_BIN" <<'EOF' +#!/bin/sh +case "$1" in display-width) printf '%s\n' 640 ;; display-height) printf '%s\n' 480 ;; *) exit 1 ;; esac +EOF + chmod +x "$BLOOM_PLATFORM_BIN" +} + +teardown() { teardown_bloom_fixture; } + +@test "canonical display selections and observed framebuffer feed one composer" { + run "$PREPARE" "$REQUEST" + + [ "$status" -eq 0 ] + printf '%s' "$output" | "$BLOOM_JQ_BIN" -e '.status == "applied"' >/dev/null + grep -F "write-display-config $BLOOM_SESSION_ROOT/ra.cfg fill nearest none none 640 480" "$MOCK_LOG" + [ "$(stat -c '%a' "$BLOOM_SESSION_ROOT/ra.cfg")" = 600 ] +} + +@test "older schema-1 settings without display default every axis to auto" { + cat >"$BLOOM_SETTINGS_BIN" <<'EOF' +#!/bin/sh +[ "$1" = values ] || exit 1 +printf '%s\n' '{"schema":1,"service":"bloom-settings","device":{"volume":10}}' +EOF + chmod +x "$BLOOM_SETTINGS_BIN" + + run "$PREPARE" "$REQUEST" + + [ "$status" -eq 0 ] + grep -F "write-display-config $BLOOM_SESSION_ROOT/ra.cfg auto auto auto auto 640 480" "$MOCK_LOG" + [ "$(stat -c '%a' "$BLOOM_SESSION_ROOT/ra.cfg")" = 600 ] +} + +@test "malformed canonical display settings fail without a session config" { + cat >"$BLOOM_SETTINGS_BIN" <<'EOF' +#!/bin/sh +printf '%s\n' '{"schema":1,"service":"bloom-settings","display":{"scaling":"../../bad","sampling":"nearest","screen_effect":"none","bezel":"none"}}' +EOF + chmod +x "$BLOOM_SETTINGS_BIN" + cat >"$BLOOM_LAUNCH_BIN" <<'EOF' +#!/bin/sh +case "$1" in validate) exit 0 ;; get) printf '%s\n' retroarch ;; *) exit 1 ;; esac +EOF + chmod +x "$BLOOM_LAUNCH_BIN" + + run "$PREPARE" "$REQUEST" + + [ "$status" -ne 0 ] + [ ! -e "$BLOOM_SESSION_ROOT/ra.cfg" ] +} diff --git a/test/shell/legacy_manifest.bats b/test/shell/legacy_manifest.bats index e6b5d95e0..fb194f743 100644 --- a/test/shell/legacy_manifest.bats +++ b/test/shell/legacy_manifest.bats @@ -6,7 +6,7 @@ --manifest /workspace/build/legacy-manifest.json [ "$status" -eq 0 ] - [[ "$output" == "legacy manifest validate: 335 components" ]] + [[ "$output" == "legacy manifest validate: 336 components" ]] [ ! -e "/workspace/static/packages/RApp/SNK - Neo Geo (GnGeo)" ] [ ! -e "/workspace/static/packages/RApp/PICO-8 (PICO-8 standalone)" ] [ ! -e "/workspace/static/packages/RApp/SCUMM (ScummVM standalone)" ] @@ -44,6 +44,7 @@ assert components["runtime-tmp-update-bin-bloomctl"]["path"] == "static/build/.t assert components["runtime-tmp-update-bin-bloom-ra-login"]["path"] == "static/build/.tmp_update/bin/bloom-ra-login" assert components["runtime-tmp-update-bin-bloom-listener-audit"]["resolution"] == "source-build" assert components["runtime-tmp-update-bin-bloom-dev-deploy"]["resolution"] == "source-build" +assert components["runtime-tmp-update-bin-bloom-display-session-prepare"]["resolution"] == "source-build" assert components["runtime-tmp-update-bin-bloom-transfer-policy"]["resolution"] == "source-build" assert components["runtime-tmp-update-bin-bloom-network-transaction"]["resolution"] == "source-build" assert components["runtime-tmp-update-bin-bloom-remote-keys"]["resolution"] == "source-build" @@ -104,7 +105,7 @@ import sys components = json.load(open(sys.argv[1], encoding="utf-8"))["components"] runtime = [item for item in components if item["id"].startswith("runtime-")] bloom = [item for item in runtime if item["source"] == "https://github.com/boburning/BloomOS"] -assert len(bloom) == 58 +assert len(bloom) == 59 assert all(item["resolution"] == "source-build" for item in bloom) assert all(item["source_revision"] == "release-commit" for item in bloom) PY diff --git a/test/test_bloom_launch.cpp b/test/test_bloom_launch.cpp index 8be026d30..c20ab47f4 100644 --- a/test/test_bloom_launch.cpp +++ b/test/test_bloom_launch.cpp @@ -8,6 +8,7 @@ extern "C" { #include "../src/bloomLaunch/bloom_launch.h" +#include "../src/bloomLaunch/bloom_retroarch_config.h" } namespace { @@ -486,6 +487,46 @@ TEST_F(BloomLaunchTest, WritesSessionOnlyRaConfigAndLeavesPermanentConfigUntouch std::filesystem::remove(temporary); } +TEST_F(BloomLaunchTest, DisplayAndAchievementsShareOneTrustedSessionConfig) +{ + ASSERT_EQ(0, bloom_launch_create_file( + request_.c_str(), + "bloom-game-v1:2d514749ed2f60ba7a6583d7e36483b113005fd788bab176fc9941256551ad71", + "gb", "/mnt/SDCARD/Roms/GB/Bob's $pecial 游戏.zip", + "/mnt/SDCARD/Emu/GB/launch.sh", "retroarch", "gambatte_libretro.so", 0, + error_, sizeof(error_))); + std::filesystem::create_directories("/tmp/bloom-session"); + auto config_path = std::filesystem::path("/tmp/bloom-session") / + ("display-merge-" + std::to_string(getpid()) + ".cfg"); + char scaling[24] = {}; + char effect[16] = {}; + char bezel[16] = {}; + char reason[48] = {}; + ASSERT_EQ(0, bloom_launch_write_display_config( + config_path.c_str(), "fill", "linear", "none", "none", 640, 480, + scaling, sizeof(scaling), effect, sizeof(effect), bezel, sizeof(bezel), + reason, sizeof(reason), error_, sizeof(error_))) + << error_; + ASSERT_EQ(0, bloom_launch_set_achievements(request_.c_str(), 1, "softcore", "direct", 1234, + "verified", error_, sizeof(error_))); + ASSERT_EQ(0, bloom_launch_write_ra_config(request_.c_str(), config_path.c_str(), "BloomUser", + "token123", nullptr, error_, sizeof(error_))) + << error_; + ASSERT_EQ(0, bloom_launch_attach_session_config(request_.c_str(), config_path.c_str(), + error_, sizeof(error_))) + << error_; + std::ifstream config_file(config_path); + std::string config((std::istreambuf_iterator(config_file)), + std::istreambuf_iterator()); + EXPECT_NE(config.find("cheevos_enable = \"true\""), std::string::npos); + EXPECT_NE(config.find("video_fullscreen = \"true\""), std::string::npos); + EXPECT_NE(config.find("custom_viewport_width = \"640\""), std::string::npos); + EXPECT_NE(config.find("video_smooth = \"true\""), std::string::npos); + EXPECT_NE(config.find("video_filter = \"\""), std::string::npos); + EXPECT_EQ(0, bloom_retroarch_config_validate_file(config_path.c_str(), error_, sizeof(error_))); + std::filesystem::remove(config_path); +} + TEST_F(BloomLaunchTest, RejectsCorePathsInsteadOfCoreBasenames) { write_request(); diff --git a/test/test_bloom_retroarch_config.cpp b/test/test_bloom_retroarch_config.cpp index 4c8b88ba5..8d0bf6387 100644 --- a/test/test_bloom_retroarch_config.cpp +++ b/test/test_bloom_retroarch_config.cpp @@ -136,4 +136,78 @@ TEST_F(BloomRetroarchConfigTest, ValidatesPrivateRegularCanonicalFiles) EXPECT_NE(0, bloom_retroarch_config_validate_file(link.c_str(), error_, sizeof(error_))); } +TEST_F(BloomRetroarchConfigTest, AppliesIndependentDisplayAxesAndSafeUnavailableFallbacks) +{ + BloomRetroarchConfig *config = bloom_retroarch_config_create(); + ASSERT_NE(nullptr, config); + char scaling[24] = {}; + char effect[16] = {}; + char bezel[16] = {}; + char reason[48] = {}; + ASSERT_EQ(0, bloom_retroarch_config_apply_display( + config, "fill", "nearest", "none", "none", 640, 480, + scaling, sizeof(scaling), effect, sizeof(effect), bezel, sizeof(bezel), + reason, sizeof(reason), error_, sizeof(error_))) + << error_; + EXPECT_STREQ("fill", scaling); + EXPECT_STREQ("none", effect); + EXPECT_STREQ("none", bezel); + EXPECT_STREQ("", reason); + char output[4096] = {}; + size_t written = 0; + ASSERT_EQ(0, bloom_retroarch_config_render(config, output, sizeof(output), &written, + error_, sizeof(error_))) + << error_; + std::string rendered(output, written); + EXPECT_NE(rendered.find("video_fullscreen = \"true\""), std::string::npos); + EXPECT_NE(rendered.find("custom_viewport_width = \"640\""), std::string::npos); + EXPECT_NE(rendered.find("custom_viewport_height = \"480\""), std::string::npos); + EXPECT_NE(rendered.find("video_dingux_ipu_filter_type = \"0\""), std::string::npos); + EXPECT_NE(rendered.find("video_smooth = \"false\""), std::string::npos); + EXPECT_NE(rendered.find("video_filter = \"\""), std::string::npos); + EXPECT_NE(rendered.find("input_overlay_enable = \"false\""), std::string::npos); + bloom_retroarch_config_destroy(config); + + config = bloom_retroarch_config_create(); + ASSERT_NE(nullptr, config); + ASSERT_EQ(0, bloom_retroarch_config_apply_display( + config, "integer-pixels", "auto", "lcd", "custom", 640, 480, + scaling, sizeof(scaling), effect, sizeof(effect), bezel, sizeof(bezel), + reason, sizeof(reason), error_, sizeof(error_))) + << error_; + EXPECT_STREQ("auto", scaling); + EXPECT_STREQ("auto", effect); + EXPECT_STREQ("auto", bezel); + EXPECT_STREQ("source_geometry_unavailable", reason); + memset(output, 0, sizeof(output)); + written = 0; + ASSERT_EQ(0, bloom_retroarch_config_render(config, output, sizeof(output), &written, + error_, sizeof(error_))); + EXPECT_EQ("video_fullscreen = \"true\"\n", std::string(output, written)); + bloom_retroarch_config_destroy(config); +} + +TEST_F(BloomRetroarchConfigTest, ImportsDisplayConfigBeforeAddingOtherOwnedContributions) +{ + auto existing = root_ / "display.cfg"; + std::ofstream(existing) << "video_fullscreen = \"true\"\n" + "video_smooth = \"false\"\n"; + ASSERT_EQ(0, chmod(existing.c_str(), 0600)); + BloomRetroarchConfig *config = bloom_retroarch_config_create(); + ASSERT_NE(nullptr, config); + ASSERT_EQ(0, bloom_retroarch_config_import_file(config, existing.c_str(), error_, sizeof(error_))) + << error_; + ASSERT_EQ(0, bloom_retroarch_config_set_bool(config, BLOOM_RA_CONFIG_ACHIEVEMENTS, + "cheevos_enable", 1, error_, sizeof(error_))); + char output[1024] = {}; + size_t written = 0; + ASSERT_EQ(0, bloom_retroarch_config_render(config, output, sizeof(output), &written, + error_, sizeof(error_))); + EXPECT_EQ(std::string("cheevos_enable = \"true\"\n" + "video_fullscreen = \"true\"\n" + "video_smooth = \"false\"\n"), + std::string(output, written)); + bloom_retroarch_config_destroy(config); +} + } // namespace diff --git a/test/test_bloom_settings.cpp b/test/test_bloom_settings.cpp index 453576519..32a823777 100644 --- a/test/test_bloom_settings.cpp +++ b/test/test_bloom_settings.cpp @@ -794,7 +794,7 @@ TEST_F(BloomSettingsTest, ImportsLegacyNetworkPreferencesOnceAndReadsCanonicalVa int value = -1; EXPECT_NE(0, bloom_settings_get_network_boolean(settings.c_str(), "network.ssh_enabled", - &value, error, sizeof(error))); + &value, error, sizeof(error))); BloomSettingsNetworkImportResult migration{}; ASSERT_EQ(0, bloom_settings_import_network_v1(settings.c_str(), 1, 0, 1, &migration, error, sizeof(error))) @@ -802,13 +802,13 @@ TEST_F(BloomSettingsTest, ImportsLegacyNetworkPreferencesOnceAndReadsCanonicalVa EXPECT_EQ(1, migration.changed); EXPECT_EQ(2, migration.generation); ASSERT_EQ(0, bloom_settings_get_network_boolean(settings.c_str(), "network.ssh_enabled", - &value, error, sizeof(error))); + &value, error, sizeof(error))); EXPECT_EQ(1, value); ASSERT_EQ(0, bloom_settings_get_network_boolean(settings.c_str(), "network.sftp_enabled", - &value, error, sizeof(error))); + &value, error, sizeof(error))); EXPECT_EQ(0, value); ASSERT_EQ(0, bloom_settings_get_network_boolean(settings.c_str(), "network.samba_enabled", - &value, error, sizeof(error))); + &value, error, sizeof(error))); EXPECT_EQ(1, value); BloomSettingsNetworkImportResult repeated{}; @@ -817,7 +817,7 @@ TEST_F(BloomSettingsTest, ImportsLegacyNetworkPreferencesOnceAndReadsCanonicalVa EXPECT_EQ(0, repeated.changed); EXPECT_EQ(2, repeated.generation); ASSERT_EQ(0, bloom_settings_get_network_boolean(settings.c_str(), "network.ssh_enabled", - &value, error, sizeof(error))); + &value, error, sizeof(error))); EXPECT_EQ(1, value); } @@ -838,8 +838,8 @@ TEST_F(BloomSettingsTest, CanonicalNetworkMutationIsAllowlistedAndTyped) EXPECT_EQ(1, result.materialized); int value = 0; ASSERT_EQ(0, bloom_settings_get_network_boolean(settings.c_str(), - "network.sftp_enabled", &value, error, - sizeof(error))); + "network.sftp_enabled", &value, error, + sizeof(error))); EXPECT_EQ(1, value); BloomSettingsMutationResult rejected{}; EXPECT_NE(0, bloom_settings_set(settings.c_str(), system.c_str(), config.c_str(), @@ -915,6 +915,62 @@ TEST_F(BloomSettingsTest, ThemeVariantMutationUpgradesExistingCanonicalSettings) EXPECT_STREQ("Dense", after.theme_variant); } +TEST_F(BloomSettingsTest, DisplaySelectionsAreTypedCanonicalAndNeverMaterializedToOnion) +{ + write(system, R"({"vol":5,"unknown_display_owner":"legacy"})"); + BloomSettingsImportResult imported{}; + char error[160] = {}; + ASSERT_EQ(0, bloom_settings_import_onion(system.c_str(), config.c_str(), settings.c_str(), + snapshot.c_str(), &imported, error, sizeof(error))); + BloomSettingsAuthorityResult activated{}; + ASSERT_EQ(0, bloom_settings_activate(settings.c_str(), system.c_str(), config.c_str(), + &activated, error, sizeof(error))); + + cJSON *canonical = load_settings(); + ASSERT_TRUE(cJSON_IsObject(canonical)); + cJSON_DeleteItemFromObjectCaseSensitive(canonical, "display"); + char *older = cJSON_PrintUnformatted(canonical); + ASSERT_NE(nullptr, older); + write(settings, older); + cJSON_free(older); + cJSON_Delete(canonical); + + BloomSettingsValues defaults{}; + ASSERT_EQ(0, bloom_settings_read_values(settings.c_str(), &defaults, error, sizeof(error))); + EXPECT_STREQ("auto", defaults.display_scaling); + EXPECT_STREQ("auto", defaults.display_sampling); + EXPECT_STREQ("auto", defaults.display_screen_effect); + EXPECT_STREQ("auto", defaults.display_bezel); + + BloomSettingsMutationResult scaling{}; + ASSERT_EQ(0, bloom_settings_set(settings.c_str(), system.c_str(), config.c_str(), + "display.scaling", "fill", &scaling, error, + sizeof(error))) + << error; + BloomSettingsMutationResult sampling{}; + ASSERT_EQ(0, bloom_settings_set(settings.c_str(), system.c_str(), config.c_str(), + "display.sampling", "nearest", &sampling, error, + sizeof(error))) + << error; + BloomSettingsMutationResult rejected{}; + EXPECT_NE(0, bloom_settings_set(settings.c_str(), system.c_str(), config.c_str(), + "display.screen_effect", "scanlines", &rejected, error, + sizeof(error))); + + BloomSettingsValues values{}; + ASSERT_EQ(0, bloom_settings_read_values(settings.c_str(), &values, error, sizeof(error))); + EXPECT_STREQ("fill", values.display_scaling); + EXPECT_STREQ("nearest", values.display_sampling); + EXPECT_STREQ("auto", values.display_screen_effect); + EXPECT_STREQ("auto", values.display_bezel); + + std::ifstream legacy_stream(system); + std::string legacy((std::istreambuf_iterator(legacy_stream)), + std::istreambuf_iterator()); + EXPECT_EQ(std::string::npos, legacy.find("display.scaling")); + EXPECT_NE(std::string::npos, legacy.find("unknown_display_owner")); +} + TEST_F(BloomSettingsTest, MaterializationFailureKeepsCanonicalMutationAuthoritative) { write(system, R"({"vol":5})"); diff --git a/tools/dev-deploy-manifest.py b/tools/dev-deploy-manifest.py index 9b38f115c..075177fcb 100755 --- a/tools/dev-deploy-manifest.py +++ b/tools/dev-deploy-manifest.py @@ -36,6 +36,7 @@ "bin/bloom-launch", "bin/bloom-launch-run", "bin/bloom-launch-override", + "bin/bloom-display-session-prepare", "bin/bloom-session", "bin/bloom-ra", "bin/bloom-ra-session-prepare", diff --git a/tools/legacy_manifest.py b/tools/legacy_manifest.py index b0c22286f..68ca6cad8 100644 --- a/tools/legacy_manifest.py +++ b/tools/legacy_manifest.py @@ -52,6 +52,7 @@ "runtime-tmp-update-bin-bloom-dev-deploy", "runtime-tmp-update-bin-bloom-dev-ssh", "runtime-tmp-update-bin-bloom-diagnostics-export", + "runtime-tmp-update-bin-bloom-display-session-prepare", "runtime-tmp-update-bin-bloom-controls", "runtime-tmp-update-bin-bloom-game-smoke", "runtime-tmp-update-bin-bloom-health-system",