/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" #include "scripting/wasmscreens.h" #include "scripting/wasmscreenkernels.h" #include "input/command.h" #include "interfaces/ITexture.h" #include "utilities/Globals.h" #include "utilities/Logs.h" #include "utilities/dictionary.h" #include "utilities/utilities.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { // the wasm stack of a screen, in bytes constexpr std::uint32_t stack_size{64 * 1024}; // the most instructions one call into a screen may run, so that a screen stuck in a loop can't hang the worker constexpr int instruction_limit{200'000'000}; // the largest side of a screen image, in pixels constexpr int max_image_side{4096}; constexpr std::size_t no_texture{std::numeric_limits::max()}; // the type of a value in the input of a screen, as eu07.value_type() reports it enum class value_type : std::int32_t { none = 0, floating = 1, integer = 2, boolean = 3, string = 4, vec2_list = 5 }; // the input of one refresh, indexed by key. when two values share a key the later one counts, as it does for the // python screens, which get the values in the same order class input_view { public: struct entry { value_type type{value_type::none}; std::size_t index{0}; }; explicit input_view(dictionary_source const &Source) : m_source{Source} { index(Source.floats, value_type::floating); index(Source.integers, value_type::integer); index(Source.bools, value_type::boolean); index(Source.strings, value_type::string); index(Source.vec2_lists, value_type::vec2_list); } entry find(std::string_view const Key) const { if (auto const lookup{m_entries.find(Key)}; lookup != m_entries.end()) { return lookup->second; } return {}; } dictionary_source const &source() const { return m_source; } private: template void index(Sequence_ const &Values, value_type const Type) { for (std::size_t idx = 0; idx < Values.size(); ++idx) { m_entries.insert_or_assign(std::string_view{Values[idx].first}, entry{Type, idx}); } } dictionary_source const &m_source; std::unordered_map m_entries; }; struct pending_command { std::string name; std::int32_t param1{0}; std::int32_t param2{0}; std::uint32_t vehicle{0}; }; // the length of the UTF-8 sequence starting with the byte, 0 for a byte that can't start one std::size_t utf8_length(std::uint8_t const Lead) { if (Lead < 0x80) { return 1; } if (Lead >= 0xC2 && Lead <= 0xDF) { return 2; } if (Lead >= 0xE0 && Lead <= 0xEF) { return 3; } if (Lead >= 0xF0 && Lead <= 0xF4) { return 4; } return 0; } // true for text in valid UTF-8 bool is_utf8(std::string_view const Text) { std::size_t idx{0}; while (idx < Text.size()) { auto const length{utf8_length(static_cast(Text[idx]))}; if (length == 0 || idx + length > Text.size()) { return false; } auto const sequence{Text.substr(idx + 1, length - 1)}; if (false == std::ranges::all_of(sequence, [](char const Byte) { return (static_cast(Byte) & 0xC0) == 0x80; })) { return false; } idx += length; } return true; } std::int32_t to_int32(double const Value) { if (std::isnan(Value)) { return 0; } return static_cast(std::clamp(Value, static_cast(std::numeric_limits::min()), static_cast(std::numeric_limits::max()))); } std::int32_t to_int32(std::size_t const Value) { return static_cast(std::min(Value, std::numeric_limits::max())); } } // namespace class wasm_screen { public: wasm_screen(wasm_screen_queue::screen_request const &Request, wasm_screens::frame_schedule const &Schedule) : kind{Request.kind}, script{Request.script}, folder{substr_path(Request.script)}, texture{Request.texture}, m_schedule{Schedule} { } ~wasm_screen() { release_runtime(); } wasm_screen(wasm_screen const &) = delete; wasm_screen &operator=(wasm_screen const &) = delete; bool poll(std::uint64_t const Frame) { std::scoped_lock lock{m_schedule_mutex}; return m_schedule.poll(Frame); } // frees the module instance. done by the queue when it stops, as the runtime goes with it void release_runtime() { if (exec_env != nullptr) { wasm_runtime_destroy_exec_env(exec_env); exec_env = nullptr; } if (instance != nullptr) { wasm_runtime_deinstantiate(instance); instance = nullptr; } failed = true; } // set when made, read-only afterwards wasm_screen_kind const kind; std::string const script; std::string const folder; ITexture *const texture; // set by the worker when the screen can't run (any more); a failed screen isn't refreshed std::atomic failed{false}; // the worker's part wasm_module_inst_t instance{nullptr}; wasm_exec_env_t exec_env{nullptr}; wasm_function_inst_t render{nullptr}; wasm_function_inst_t width{nullptr}; wasm_function_inst_t height{nullptr}; bool commands_ignored{false}; // a screen outside the cab tried to send a command, and it was logged // the call in progress input_view const *input{nullptr}; std::vector *commands{nullptr}; std::uint32_t vehicle{0}; // the image, handed from the worker to the main thread std::mutex image_mutex; std::vector image; int image_width{0}; int image_height{0}; bool image_pending{false}; // drawn and not yet uploaded std::size_t uploaded_texture{no_texture}; // the gl texture the image went to, to upload it again when it's made anew private: std::mutex m_schedule_mutex; wasm_screens::frame_schedule m_schedule; }; namespace { wasm_screen *screen_of(wasm_exec_env_t Env) { return static_cast(wasm_runtime_get_custom_data(wasm_runtime_get_module_inst(Env))); } struct lookup_result { input_view const *view{nullptr}; input_view::entry entry; }; lookup_result lookup(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length) { auto const *screen{screen_of(Env)}; if (screen == nullptr || screen->input == nullptr || Key == nullptr) { return {}; } return {screen->input, screen->input->find(std::string_view{Key, Length})}; } // the natives of the "eu07" module, which a screen imports. see wasm_screens_doc.md std::int32_t native_value_type(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length) { return static_cast(lookup(Env, Key, Length).entry.type); } double native_get_float(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length) { auto const [view, entry]{lookup(Env, Key, Length)}; using enum value_type; switch (entry.type) { case floating: return view->source().floats[entry.index].second; case integer: return view->source().integers[entry.index].second; case boolean: return view->source().bools[entry.index].second ? 1.0 : 0.0; default: return 0.0; } } std::int32_t native_get_int(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length) { auto const [view, entry]{lookup(Env, Key, Length)}; using enum value_type; switch (entry.type) { case floating: return to_int32(view->source().floats[entry.index].second); case integer: return view->source().integers[entry.index].second; case boolean: return view->source().bools[entry.index].second ? 1 : 0; default: return 0; } } std::int32_t native_get_bool(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length) { auto const [view, entry]{lookup(Env, Key, Length)}; using enum value_type; switch (entry.type) { case floating: return view->source().floats[entry.index].second != 0.0 ? 1 : 0; case integer: return view->source().integers[entry.index].second != 0 ? 1 : 0; case boolean: return view->source().bools[entry.index].second ? 1 : 0; default: return 0; } } // copies the string into the buffer, as UTF-8 (names of sceneries and vehicles may be in windows-1250). // returns the length of the whole string, which may be more than the buffer took, or -1 when there is no such string std::int32_t native_get_string(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length, char *Buffer, std::uint32_t const Capacity) { auto const [view, entry]{lookup(Env, Key, Length)}; if (entry.type != value_type::string) { return -1; } auto const &value{view->source().strings[entry.index].second}; auto const text{is_utf8(value) ? value : win1250_to_utf8(value)}; if (Buffer != nullptr) { std::memcpy(Buffer, text.data(), std::min(text.size(), Capacity)); } return to_int32(text.size()); } std::int32_t native_get_vec2_count(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length) { auto const [view, entry]{lookup(Env, Key, Length)}; if (entry.type != value_type::vec2_list) { return 0; } return to_int32(view->source().vec2_lists[entry.index].second.size()); } // writes the point as two f32 at Output. returns 1 on success, 0 when there is no such point std::int32_t native_get_vec2(wasm_exec_env_t Env, char const *Key, std::uint32_t const Length, std::int32_t const Index, std::uint32_t const Output) { auto const [view, entry]{lookup(Env, Key, Length)}; if (entry.type != value_type::vec2_list || Index < 0) { return 0; } auto const &points{view->source().vec2_lists[entry.index].second}; if (static_cast(Index) >= points.size()) { return 0; } auto *instance{wasm_runtime_get_module_inst(Env)}; std::array const point{points[Index].x, points[Index].y}; if (false == wasm_runtime_validate_app_addr(instance, Output, sizeof(point))) { // the runtime has raised an exception, which ends the call return 0; } std::memcpy(wasm_runtime_addr_app_to_native(instance, Output), point.data(), sizeof(point)); return 1; } void native_command(wasm_exec_env_t Env, char const *Name, std::uint32_t const Length, std::int32_t const Param1, std::int32_t const Param2) { auto *screen{screen_of(Env)}; if (screen == nullptr || screen->commands == nullptr || Name == nullptr) { return; } if (screen->kind != wasm_screen_kind::cab) { if (false == screen->commands_ignored) { screen->commands_ignored = true; WriteLog(std::format("WASM screen \"{}\": commands are only taken from screens in the cab", screen->script)); } return; } screen->commands->emplace_back(std::string{Name, Length}, Param1, Param2, screen->vehicle); } void native_log(wasm_exec_env_t Env, char const *Text, std::uint32_t const Length) { auto const *screen{screen_of(Env)}; if (screen == nullptr || Text == nullptr) { return; } WriteLog(std::format("WASM screen \"{}\": {}", screen->script, std::string_view{Text, Length})); } // reads a file from the screen's folder into the buffer. returns the size of the whole file, which may be more // than the buffer took, or -1 when there is no such file or the path leads outside the folder std::int32_t native_file_read(wasm_exec_env_t Env, char const *Path, std::uint32_t const Length, char *Buffer, std::uint32_t const Capacity) { auto const *screen{screen_of(Env)}; if (screen == nullptr || Path == nullptr) { return -1; } std::string_view const path{Path, Length}; if (false == wasm_screens::is_safe_relative_path(path)) { return -1; } std::ifstream file{screen->folder + std::string{path}, std::ios::binary | std::ios::ate}; if (false == file.is_open()) { return -1; } auto const size{static_cast(std::max(file.tellg(), 0))}; if (Buffer != nullptr && Capacity > 0) { file.seekg(0); file.read(Buffer, static_cast(std::min(size, Capacity))); } return to_int32(size); } // seconds of a steady clock, the same for every screen, for animations that don't depend on how often the // screen is refreshed double native_time(wasm_exec_env_t /*Env*/) { static auto const start{std::chrono::steady_clock::now()}; return std::chrono::duration(std::chrono::steady_clock::now() - start).count(); } // the bytes of an image in the module's memory; an empty span for one that lies outside (the runtime then raises an // exception, which ends the call) std::span module_image(wasm_exec_env_t Env, std::uint32_t const Offset, int const Width, int const Height) { auto *instance{wasm_runtime_get_module_inst(Env)}; auto const size{wasm_screens::image_size(Width, Height)}; if (false == wasm_runtime_validate_app_addr(instance, Offset, size)) { return {}; } return {static_cast(wasm_runtime_addr_app_to_native(instance, Offset)), size}; } bool valid_side(std::int32_t const Side) { return Side > 0 && Side <= max_image_side; } // led_util's render_post, natively: Target takes (2 Width + 2) x (2 Height + 2) pixels. returns 1, or 0 for an image // that doesn't fit std::int32_t native_led_post(wasm_exec_env_t Env, std::uint32_t const Source, std::int32_t const Width, std::int32_t const Height, std::uint32_t const Target, std::int32_t const Bloom) { if (false == valid_side(Width) || false == valid_side(Height)) { return 0; } auto const source{module_image(Env, Source, Width, Height)}; auto const target{module_image(Env, Target, Width * 2 + 2, Height * 2 + 2)}; if (source.empty() || target.empty()) { return 0; } return wasm_screens::led_post(source, Width, Height, target, Bloom != 0) ? 1 : 0; } // Pillow's resize(..., LANCZOS), natively. returns 1, or 0 for an image that doesn't fit std::int32_t native_resize_lanczos(wasm_exec_env_t Env, std::uint32_t const Source, std::int32_t const Width, std::int32_t const Height, std::uint32_t const Target, std::int32_t const TargetWidth, std::int32_t const TargetHeight) { if (false == valid_side(Width) || false == valid_side(Height) || false == valid_side(TargetWidth) || false == valid_side(TargetHeight)) { return 0; } auto const source{module_image(Env, Source, Width, Height)}; auto const target{module_image(Env, Target, TargetWidth, TargetHeight)}; if (source.empty() || target.empty()) { return 0; } return wasm_screens::resize_lanczos(source, Width, Height, target, TargetWidth, TargetHeight) ? 1 : 0; } template NativeSymbol native(char const *Name, Function_ *Function, char const *Signature) { return NativeSymbol{Name, reinterpret_cast(Function), Signature, nullptr}; } } // namespace struct wasm_screen_queue::implementation { struct module_entry { std::vector bytes; // the runtime reads the module from here as long as it is loaded wasm_module_t module{nullptr}; }; struct task { std::shared_ptr screen; std::shared_ptr input; std::uint32_t vehicle{0}; }; implementation() : natives{native("value_type", &native_value_type, "(*~)i"), native("get_float", &native_get_float, "(*~)F"), native("get_int", &native_get_int, "(*~)i"), native("get_bool", &native_get_bool, "(*~)i"), native("get_string", &native_get_string, "(*~*~)i"), native("get_vec2_count", &native_get_vec2_count, "(*~)i"), native("get_vec2", &native_get_vec2, "(*~ii)i"), native("command", &native_command, "(*~ii)"), native("log", &native_log, "(*~)"), native("file_read", &native_file_read, "(*~*~)i"), native("time", &native_time, "()F"), native("led_post", &native_led_post, "(iiiii)i"), native("resize_lanczos", &native_resize_lanczos, "(iiiiii)i")} { } void run(std::stop_token const &Stop); void process(task const &Task); bool instantiate(wasm_screen &Screen); wasm_module_t load_module(std::string const &Script); bool call(wasm_screen &Screen, wasm_function_inst_t Function, std::string_view Name, std::int32_t &Result) const; void present(wasm_screen &Screen, std::uint32_t Pixels) const; static void upload(wasm_screen &Screen); static void execute(pending_command const &Command); // the runtime keeps the array and sorts it, so it lives as long as the queue std::array natives; bool initialized{false}; std::atomic frame{0}; std::mutex screens_mutex; std::unordered_map> screens; std::mutex tasks_mutex; std::condition_variable_any tasks_condition; std::deque tasks; std::jthread worker; std::mutex commands_mutex; std::vector commands; // the worker's, and the queue's once the worker stopped std::unordered_map modules; }; void wasm_screen_queue::implementation::run(std::stop_token const &Stop) { wasm_runtime_init_thread_env(); while (false == Stop.stop_requested()) { task next; { std::unique_lock lock{tasks_mutex}; if (false == tasks_condition.wait(lock, Stop, [this] { return false == tasks.empty(); })) { break; } next = std::move(tasks.front()); tasks.pop_front(); } process(next); } wasm_runtime_destroy_thread_env(); } void wasm_screen_queue::implementation::process(task const &Task) { auto &screen{*Task.screen}; if (screen.failed) { return; } if (screen.instance == nullptr && false == instantiate(screen)) { screen.failed = true; return; } input_view const view{*Task.input}; std::vector sent; screen.input = &view; screen.commands = &sent; screen.vehicle = Task.vehicle; std::int32_t pixels{0}; auto const rendered{call(screen, screen.render, "screen_render", pixels)}; screen.input = nullptr; screen.commands = nullptr; if (false == rendered) { return; } // 0: the screen didn't change, keep the image there is if (pixels != 0) { present(screen, static_cast(pixels)); } if (false == sent.empty()) { std::scoped_lock lock{commands_mutex}; commands.insert(commands.end(), std::make_move_iterator(sent.begin()), std::make_move_iterator(sent.end())); } } bool wasm_screen_queue::implementation::instantiate(wasm_screen &Screen) { auto *module{load_module(Screen.script)}; if (module == nullptr) { return false; } std::array error{}; Screen.instance = wasm_runtime_instantiate(module, stack_size, 0, error.data(), static_cast(error.size())); if (Screen.instance == nullptr) { ErrorLog(std::format("WASM screen \"{}\": instantiation failed: {}", Screen.script, error.data())); return false; } wasm_runtime_set_custom_data(Screen.instance, &Screen); Screen.exec_env = wasm_runtime_create_exec_env(Screen.instance, stack_size); if (Screen.exec_env == nullptr) { ErrorLog(std::format("WASM screen \"{}\": failed to create the execution environment", Screen.script)); return false; } Screen.render = wasm_runtime_lookup_function(Screen.instance, "screen_render"); Screen.width = wasm_runtime_lookup_function(Screen.instance, "screen_width"); Screen.height = wasm_runtime_lookup_function(Screen.instance, "screen_height"); if (Screen.render == nullptr || Screen.width == nullptr || Screen.height == nullptr) { ErrorLog(std::format("WASM screen \"{}\": the module has to export screen_render, screen_width and screen_height", Screen.script)); return false; } if (auto *init{wasm_runtime_lookup_function(Screen.instance, "screen_init")}; init != nullptr) { std::int32_t result{0}; if (false == call(Screen, init, "screen_init", result)) { return false; } if (result != 0) { ErrorLog(std::format("WASM screen \"{}\": screen_init returned {}", Screen.script, result)); return false; } } WriteLog(std::format("WASM screen \"{}\": ready", Screen.script)); return true; } wasm_module_t wasm_screen_queue::implementation::load_module(std::string const &Script) { if (auto const lookup{modules.find(Script)}; lookup != modules.end()) { return lookup->second.module; } // a failure is kept as well, so the module isn't tried again for every screen using it auto &entry{modules[Script]}; std::ifstream file{Script, std::ios::binary}; if (false == file.is_open()) { ErrorLog(std::format("WASM screen \"{}\": file not found", Script)); return nullptr; } entry.bytes.assign(std::istreambuf_iterator{file}, std::istreambuf_iterator{}); if (entry.bytes.empty() || entry.bytes.size() > std::numeric_limits::max()) { ErrorLog(std::format("WASM screen \"{}\": invalid module size", Script)); return nullptr; } std::array error{}; entry.module = wasm_runtime_load(entry.bytes.data(), static_cast(entry.bytes.size()), error.data(), static_cast(error.size())); if (entry.module == nullptr) { ErrorLog(std::format("WASM screen \"{}\": failed to load the module: {}", Script, error.data())); } return entry.module; } // calls a function of the screen that takes nothing and returns an i32. a failed call disables the screen bool wasm_screen_queue::implementation::call(wasm_screen &Screen, wasm_function_inst_t Function, std::string_view const Name, std::int32_t &Result) const { wasm_runtime_set_instruction_count_limit(Screen.exec_env, instruction_limit); std::array cells{}; if (false == wasm_runtime_call_wasm(Screen.exec_env, Function, 0, cells.data())) { auto const *exception{wasm_runtime_get_exception(Screen.instance)}; ErrorLog(std::format("WASM screen \"{}\": {} failed: {}", Screen.script, Name, exception != nullptr ? exception : "unknown error")); Screen.failed = true; return false; } Result = static_cast(cells[0]); return true; } // copies the image the screen drew, Pixels being its address in the module's memory void wasm_screen_queue::implementation::present(wasm_screen &Screen, std::uint32_t const Pixels) const { std::int32_t width{0}; std::int32_t height{0}; if (false == call(Screen, Screen.width, "screen_width", width) || false == call(Screen, Screen.height, "screen_height", height)) { return; } if (width <= 0 || height <= 0 || width > max_image_side || height > max_image_side) { ErrorLog(std::format("WASM screen \"{}\": invalid image size {}x{}", Screen.script, width, height)); Screen.failed = true; return; } auto const size{static_cast(width) * static_cast(height) * 4}; if (false == wasm_runtime_validate_app_addr(Screen.instance, Pixels, size)) { ErrorLog(std::format("WASM screen \"{}\": the image lies outside the module's memory", Screen.script)); Screen.failed = true; return; } auto const *pixels{static_cast(wasm_runtime_addr_app_to_native(Screen.instance, Pixels))}; std::scoped_lock lock{Screen.image_mutex}; Screen.image.assign(pixels, pixels + size); Screen.image_width = width; Screen.image_height = height; Screen.image_pending = true; } void wasm_screen_queue::implementation::upload(wasm_screen &Screen) { if (Screen.texture == nullptr) { return; } std::scoped_lock lock{Screen.image_mutex}; if (Screen.image.empty() || false == Screen.texture->get_is_ready()) { // nothing drawn yet, or the texture isn't made yet or was released: the image goes up once it's there return; } if (false == Screen.image_pending && Screen.texture->get_id() == Screen.uploaded_texture) { return; } Screen.texture->update_from_memory(Screen.image_width, Screen.image_height, Screen.image.data()); Screen.uploaded_texture = Screen.texture->get_id(); Screen.image_pending = false; } void wasm_screen_queue::implementation::execute(pending_command const &Command) { auto const lookup{simulation::commandMap.find(Command.name)}; if (lookup == simulation::commandMap.end()) { ErrorLog(std::format("WASM screen: command [{}] not found", Command.name)); return; } command_data data{}; data.command = lookup->second; data.action = GLFW_PRESS; data.param1 = Command.param1; data.param2 = Command.param2; WriteLog(std::format("WASM screen: executing command [{}] with params: P1={} P2={} Target ID={}", Command.name, Command.param1, Command.param2, Command.vehicle)); simulation::Commands.push(data, static_cast(command_target::vehicle) | Command.vehicle); } wasm_screen_queue::wasm_screen_queue() : m_impl{std::make_unique()} {} wasm_screen_queue::~wasm_screen_queue() { exit(); } bool wasm_screen_queue::init() { if (m_impl->initialized) { return true; } RuntimeInitArgs arguments{}; arguments.mem_alloc_type = Alloc_With_System_Allocator; arguments.native_module_name = "eu07"; arguments.native_symbols = m_impl->natives.data(); arguments.n_native_symbols = static_cast(m_impl->natives.size()); arguments.running_mode = Mode_Interp; if (false == wasm_runtime_full_init(&arguments)) { ErrorLog("WASM screens: failed to initialize the runtime"); return false; } wasm_runtime_set_log_level(WASM_LOG_LEVEL_ERROR); m_impl->initialized = true; auto *impl{m_impl.get()}; m_impl->worker = std::jthread([impl](std::stop_token const &Stop) { impl->run(Stop); }); WriteLog(std::format("WASM screens: setup complete, refreshed every {} frames in the cab and every {} frames outside", Global.wasm_cabframes, Global.wasm_externalframes)); return true; } void wasm_screen_queue::exit() { if (false == m_impl->initialized) { return; } if (m_impl->worker.joinable()) { m_impl->worker.request_stop(); m_impl->worker.join(); } { std::scoped_lock lock{m_impl->tasks_mutex}; m_impl->tasks.clear(); } { std::scoped_lock lock{m_impl->commands_mutex}; m_impl->commands.clear(); } { // the vehicles may hold their screens past this point, but the runtime goes now std::scoped_lock lock{m_impl->screens_mutex}; for (auto const &[key, screen] : m_impl->screens) { if (auto const live{screen.lock()}; live != nullptr) { live->release_runtime(); } } m_impl->screens.clear(); } for (auto const &[script, entry] : m_impl->modules) { if (entry.module != nullptr) { wasm_runtime_unload(entry.module); } } m_impl->modules.clear(); wasm_runtime_destroy(); m_impl->initialized = false; } std::shared_ptr wasm_screen_queue::acquire(screen_request const &Request) { if (false == m_impl->initialized || Request.script.empty()) { return nullptr; } auto const key{Request.key.empty() ? Request.script : Request.key}; std::scoped_lock lock{m_impl->screens_mutex}; if (auto const lookup{m_impl->screens.find(key)}; lookup != m_impl->screens.end()) { if (auto existing{lookup->second.lock()}; existing != nullptr) { return existing; } } auto const configured{Request.kind == wasm_screen_kind::cab ? Global.wasm_cabframes : Global.wasm_externalframes}; auto const interval{wasm_screens::frame_schedule::effective_interval(configured, Request.frames)}; auto screen{std::make_shared(Request, wasm_screens::frame_schedule{m_impl->frame.load(), interval, wasm_screens::frame_schedule::phase(key, interval)})}; m_impl->screens.insert_or_assign(key, screen); return screen; } bool wasm_screen_queue::due(wasm_screen &Screen) const { if (false == m_impl->initialized || Screen.failed) { return false; } return Screen.poll(m_impl->frame.load()); } void wasm_screen_queue::submit(std::shared_ptr const &Screen, std::shared_ptr Input, std::uint32_t const Vehicle) { if (false == m_impl->initialized || Screen == nullptr || Input == nullptr || Screen->failed) { return; } { std::scoped_lock lock{m_impl->tasks_mutex}; auto const pending{std::ranges::find(m_impl->tasks, Screen, &implementation::task::screen)}; if (pending != m_impl->tasks.end()) { // the refresh still waiting gets the newer input pending->input = std::move(Input); pending->vehicle = Vehicle; } else { m_impl->tasks.emplace_back(Screen, std::move(Input), Vehicle); } } m_impl->tasks_condition.notify_one(); } void wasm_screen_queue::update() { if (false == m_impl->initialized) { return; } ++m_impl->frame; { std::scoped_lock lock{m_impl->screens_mutex}; std::erase_if(m_impl->screens, [](auto const &Entry) { return Entry.second.expired(); }); for (auto const &[key, screen] : m_impl->screens) { if (auto const live{screen.lock()}; live != nullptr) { implementation::upload(*live); } } } std::vector sent; { std::scoped_lock lock{m_impl->commands_mutex}; sent.swap(m_impl->commands); } for (auto const &command : sent) { implementation::execute(command); } }