18
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-15 22:14:10 +01:00

wasm display support

This commit is contained in:
2026-11-10 00:57:37 +01:00
parent 685258d787
commit 18c333b089
25 changed files with 2521 additions and 23 deletions

4
.gitmodules vendored
View File

@@ -32,3 +32,7 @@
[submodule "betterRenderer/thirdparty/bvh"]
path = betterRenderer/thirdparty/bvh
url = https://github.com/madmann91/bvh.git
[submodule "ref/wamr"]
path = ref/wamr
url = https://github.com/bytecodealliance/wasm-micro-runtime
shallow = true

View File

@@ -72,6 +72,7 @@ option(WITH_OPENGL_LEGACY "Compile with OpenGL legacy renderer" ON)
option(WITH_PYTHON "Python 3.14 embedded scripting" ON)
option(WITH_UART "Compile with libserialport" ON)
option(WITH_LUA "Compile with lua scripting support" ON)
option(WITH_WASM "WebAssembly screens (wasmscreen:, wasmdestinationsign:), run by the WAMR interpreter in ref/wamr" ON)
option(WITH_OPENVR "Compile with OpenVR" OFF)
option(WITH_ZMQ "Compile with cppzmq" OFF)
option(WITH_CRASHPAD "Compile with crashpad" OFF)
@@ -321,6 +322,13 @@ if (WITH_LUA)
set(SOURCES ${SOURCES} "scripting/lua.cpp")
endif()
if (WITH_WASM)
set(DEFINITIONS ${DEFINITIONS} "WITH_WASM")
set(SOURCES ${SOURCES} "scripting/wasmscreens.cpp")
else()
set(SOURCES ${SOURCES} "scripting/wasmscreensstub.cpp")
endif()
if (WITHDUMPGEN)
set(DEFINITIONS ${DEFINITIONS} "WITHDUMPGEN")
endif()
@@ -463,6 +471,8 @@ set(CORE_SOURCES
"utilities/parser.cpp"
"utilities/utilities.cpp"
"scripting/ladderlogic.cpp"
"scripting/wasmscreenrules.cpp"
"scripting/wasmscreenkernels.cpp"
"simulation/simulationtestrules.cpp"
"vehicle/enginestart.cpp"
"vehicle/handover.cpp"
@@ -677,6 +687,11 @@ if(WIN32 AND WITH_PYTHON)
)
endif()
if (WITH_WASM)
add_subdirectory("CMake_modules/wamr" "${CMAKE_CURRENT_BINARY_DIR}/wamr")
target_link_libraries(${PROJECT_NAME} eu07_wamr)
endif ()
if (WITH_BETTER_RENDERER)
add_subdirectory("betterRenderer")
set_target_properties(betterRenderer betterRenderer_fsr2 betterRenderer_shaders PROPERTIES FOLDER "renderers")

View File

@@ -0,0 +1,67 @@
# The WebAssembly Micro Runtime (ref/wamr) for the wasm screens (scripting/wasmscreens.cpp).
#
# Built here, in a directory of its own, because the WAMR build scripts set their configuration with
# add_definitions() and include_directories(): kept in this directory, none of it reaches the game's sources.
# Only the interpreter is built (see WAMR_BUILD_FAST_INTERP), with instruction metering so that a screen stuck in a loop
# can't hang the worker thread, and without the signal-based bounds checks, which would compete with the
# game's own crash handling.
if (WIN32)
set(WAMR_BUILD_PLATFORM "windows")
elseif (APPLE)
set(WAMR_BUILD_PLATFORM "darwin")
else ()
set(WAMR_BUILD_PLATFORM "linux")
endif ()
set(WAMR_ROOT_DIR "${CMAKE_SOURCE_DIR}/ref/wamr")
set(WAMR_BUILD_INTERP 1)
# the fast interpreter counts the instructions only where it dispatches with computed gotos (GCC, Clang): built with
# MSVC it would let a screen stuck in a loop hang the worker. the classic interpreter counts them everywhere
if (MSVC)
set(WAMR_BUILD_FAST_INTERP 0)
else ()
set(WAMR_BUILD_FAST_INTERP 1)
endif ()
set(WAMR_BUILD_AOT 0)
set(WAMR_BUILD_JIT 0)
set(WAMR_BUILD_FAST_JIT 0)
set(WAMR_BUILD_LIBC_BUILTIN 0)
set(WAMR_BUILD_LIBC_WASI 0)
set(WAMR_BUILD_MULTI_MODULE 0)
set(WAMR_BUILD_LIB_PTHREAD 0)
set(WAMR_BUILD_LIB_WASI_THREADS 0)
set(WAMR_BUILD_THREAD_MGR 0)
set(WAMR_BUILD_SIMD 0)
set(WAMR_BUILD_BULK_MEMORY 1)
set(WAMR_BUILD_REF_TYPES 1)
set(WAMR_BUILD_MINI_LOADER 0)
set(WAMR_BUILD_INSTRUCTION_METERING 1)
# NOTE: the calls into the natives use WAMR's assembler invokeNative (MASM with MSVC). its C version
# (WAMR_BUILD_INVOKE_NATIVE_GENERAL) crashes on Windows x64 as soon as a module runs
set(WAMR_DISABLE_HW_BOUND_CHECK 1)
set(WAMR_DISABLE_STACK_HW_BOUND_CHECK 1)
find_package(Threads REQUIRED)
include("${WAMR_ROOT_DIR}/build-scripts/runtime_lib.cmake")
add_library(eu07_wamr STATIC ${WAMR_RUNTIME_LIB_SOURCE})
target_include_directories(eu07_wamr INTERFACE "${WAMR_ROOT_DIR}/core/iwasm/include")
# with MSVC the WAMR headers declare the API dllimport unless told otherwise; the library is linked statically
target_compile_definitions(eu07_wamr INTERFACE "WASM_RUNTIME_API_EXTERN=")
target_link_libraries(eu07_wamr PRIVATE Threads::Threads)
if (WIN32)
target_link_libraries(eu07_wamr PRIVATE ws2_32 ntdll)
else ()
target_link_libraries(eu07_wamr PRIVATE m ${CMAKE_DL_LIBS})
endif ()
if (MSVC)
# third-party code: its warnings are not ours to fix
target_compile_options(eu07_wamr PRIVATE /W0)
else ()
target_compile_options(eu07_wamr PRIVATE -w)
endif ()
set_target_properties(eu07_wamr PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
FOLDER "libraries")

View File

@@ -425,6 +425,11 @@ int eu07_application::init(int Argc, char *Argv[])
{
m_taskqueue.init();
}
crashreport_add_info("wasm_enabled", Global.wasm_enabled ? "yes" : "no");
if (Global.wasm_enabled)
{
m_wasmqueue.init();
}
if ((result = init_modes()) != 0)
{
ErrorLog("Failed to initialize game modes");
@@ -635,6 +640,7 @@ int eu07_application::run()
EditorTerrain.update(Global.pCamera.Pos);
m_taskqueue.update();
m_wasmqueue.update();
opengl_texture::reset_unit_cache();
if (!GfxRenderer->Render())
@@ -732,6 +738,7 @@ void eu07_application::exit()
// the python workers detach their GL context on the way out, so they have to be done before the windows they use are destroyed
m_taskqueue.exit();
m_wasmqueue.exit();
step("the windows");
for (auto *window : m_windows)
{

View File

@@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/.
#include "application/applicationmode.h"
#include "scripting/PyInt.h"
#include "scripting/wasmscreens.h"
#include "network/manager.h"
#include "utilities/headtrack.h"
#ifdef WITH_UART
@@ -49,6 +50,10 @@ public:
// frees the python gil and swaps out the main thread
void
release_python_lock();
// the screens drawn by WebAssembly modules
wasm_screen_queue &
wasm_screens() {
return m_wasmqueue; }
void
exit();
void
@@ -122,6 +127,7 @@ private:
modeptr_array m_modes { nullptr }; // collection of available application behaviour modes
mode_stack m_modestack; // current behaviour mode
python_taskqueue m_taskqueue;
wasm_screen_queue m_wasmqueue;
std::vector<GLFWwindow *> m_windows;
int m_glfwversion;

View File

@@ -722,6 +722,10 @@ void
opengl_texture::make_request() {
auto const components { Split( name, '?' ) };
if( components.front().ends_with( ".wasm" ) ) {
// wasm destination signs are drawn by the vehicles showing them (wasm_screen_queue)
return;
}
auto dictionary = std::make_shared<dictionary_source>( components.back() );

1
ref/wamr Submodule

Submodule ref/wamr added at 25bd7eb63e

View File

@@ -0,0 +1,242 @@
/*
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/wasmscreenkernels.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <numbers>
#include <vector>
namespace wasm_screens
{
namespace
{
constexpr int glow_radius{6};
constexpr double glow_factor{0.08};
using light = std::array<std::int64_t, 3>;
// led_util's get_px_str: the light gathered from the diodes around, weighted 1 / (0.08 + distance squared), the sums
// taken in the same order as there to get the very same numbers
light glow_at(std::span<std::uint8_t const> const Source, int const Width, int const Height, int const X, int const Y)
{
std::array<double, 3> sum{};
auto const left{std::max(X - glow_radius, 0)};
auto const right{std::min(X + glow_radius + 1, Width)};
auto const top{std::max(Y - glow_radius, 0)};
auto const bottom{std::min(Y + glow_radius + 1, Height)};
for (auto i = left; i < right; ++i)
{
auto const m{glow_factor + static_cast<double>((i - X) * (i - X))};
if (m > 20.0)
{
continue;
}
for (auto j = top; j < bottom; ++j)
{
auto const s{m + static_cast<double>((j - Y) * (j - Y))};
if (s > 20.0)
{
continue;
}
auto const *pixel{&Source[(static_cast<std::size_t>(j) * Width + i) * 4]};
for (std::size_t channel = 0; channel < 3; ++channel)
{
sum[channel] += static_cast<double>(pixel[channel]) / s;
}
}
}
return {static_cast<std::int64_t>(sum[0] * glow_factor), static_cast<std::int64_t>(sum[1] * glow_factor), static_cast<std::int64_t>(sum[2] * glow_factor)};
}
double sinc(double X)
{
if (X == 0.0)
{
return 1.0;
}
X *= std::numbers::pi;
return std::sin(X) / X;
}
double lanczos(double const X)
{
if (X >= -3.0 && X < 3.0)
{
return sinc(X) * sinc(X / 3.0);
}
return 0.0;
}
constexpr int precision_bits{32 - 8 - 2};
// the fixed-point weights of one pass, as Pillow's Resample.c (precompute_coeffs, normalize_coeffs_8bpc) makes them
struct coefficients
{
int size{0};
std::vector<std::pair<int, int>> bounds; // first input sample and the number of samples, per output sample
std::vector<std::int64_t> weights;
coefficients(int const Input, int const Output)
{
auto const scale{static_cast<double>(Input) / static_cast<double>(Output)};
auto const filterscale{std::max(scale, 1.0)};
auto const support{3.0 * filterscale};
size = static_cast<int>(std::ceil(support)) * 2 + 1;
bounds.reserve(Output);
weights.assign(static_cast<std::size_t>(Output) * size, 0);
std::vector<double> prepared(size);
for (auto xx = 0; xx < Output; ++xx)
{
auto const center{(xx + 0.5) * scale};
auto const ss{1.0 / filterscale};
auto const xmin{std::max(static_cast<int>(center - support + 0.5), 0)};
auto const xmax{std::min(static_cast<int>(center + support + 0.5), Input) - xmin};
auto total{0.0};
for (auto x = 0; x < xmax; ++x)
{
prepared[x] = lanczos((x + xmin - center + 0.5) * ss);
total += prepared[x];
}
for (auto x = 0; x < xmax; ++x)
{
if (total != 0.0)
{
prepared[x] /= total;
}
auto const fixed{prepared[x] * static_cast<double>(1 << precision_bits)};
weights[static_cast<std::size_t>(xx) * size + x] = static_cast<std::int64_t>(prepared[x] < 0.0 ? -0.5 + fixed : 0.5 + fixed);
}
bounds.emplace_back(xmin, xmax);
}
}
static std::uint8_t clip(std::int64_t const Sum)
{
if (Sum >= (std::int64_t{1} << (precision_bits + 8)))
{
return 255;
}
if (Sum <= 0)
{
return 0;
}
return static_cast<std::uint8_t>(Sum >> precision_bits);
}
};
constexpr std::int64_t rounding{std::int64_t{1} << (precision_bits - 1)};
// one pass of the resampling, along rows (Step 4: neighbouring pixels) or along columns (Step: a row)
struct pass
{
coefficients const &weights;
std::size_t step; // bytes between the samples the pass mixes
};
std::uint8_t mix(std::uint8_t const *First, pass const &Pass, std::size_t const Index)
{
auto const count{Pass.weights.bounds[Index].second};
auto const offset{Index * static_cast<std::size_t>(Pass.weights.size)};
auto sum{rounding};
for (auto sample = 0; sample < count; ++sample)
{
sum += static_cast<std::int64_t>(First[sample * Pass.step]) * Pass.weights.weights[offset + sample];
}
return coefficients::clip(sum);
}
} // namespace
bool led_post(std::span<std::uint8_t const> const Source, int const Width, int const Height, std::span<std::uint8_t> const Target, bool const Bloom)
{
auto const targetwidth{Width * 2 + 2};
auto const targetheight{Height * 2 + 2};
if (Width <= 0 || Height <= 0 || Source.size() < image_size(Width, Height) || Target.size() < image_size(targetwidth, targetheight))
{
return false;
}
// the light of every diode; above 255 it is clamped only in the final pixels, as led_util (Pillow) does
std::vector<light> diodes(static_cast<std::size_t>(Width) * Height);
for (auto y = 0; y < Height; ++y)
{
for (auto x = 0; x < Width; ++x)
{
auto const at{(static_cast<std::size_t>(y) * Width + x) * 4};
diodes[static_cast<std::size_t>(y) * Width + x] = Bloom ? glow_at(Source, Width, Height, x, y) : light{Source[at], Source[at + 1], Source[at + 2]};
}
}
// a black frame, then the diodes
for (std::size_t idx = 0; idx < image_size(targetwidth, targetheight); ++idx)
{
Target[idx] = idx % 4 == 3 ? 255 : 0;
}
for (auto i = 0; i < Width * 2; ++i)
{
for (auto j = 0; j < Height * 2; ++j)
{
auto const &diode{diodes[static_cast<std::size_t>(j / 2) * Width + i / 2]};
auto const shade{1.0 - static_cast<double>(i % 2 + j % 2) * 0.15};
auto *pixel{&Target[(static_cast<std::size_t>(j + 1) * targetwidth + (i + 1)) * 4]};
for (std::size_t channel = 0; channel < 3; ++channel)
{
pixel[channel] = static_cast<std::uint8_t>(std::clamp<std::int64_t>(static_cast<std::int64_t>(static_cast<double>(diode[channel]) * shade), 0, 255));
}
}
}
return true;
}
bool resize_lanczos(std::span<std::uint8_t const> const Source, int const Width, int const Height, std::span<std::uint8_t> const Target, int const TargetWidth,
int const TargetHeight)
{
if (Width <= 0 || Height <= 0 || TargetWidth <= 0 || TargetHeight <= 0 || Source.size() < image_size(Width, Height) ||
Target.size() < image_size(TargetWidth, TargetHeight))
{
return false;
}
coefficients const horizontal{Width, TargetWidth};
coefficients const vertical{Height, TargetHeight};
std::vector<std::uint8_t> temporary(image_size(TargetWidth, Height));
pass const along_row{horizontal, 4};
for (auto y = 0; y < Height; ++y)
{
for (auto x = 0; x < TargetWidth; ++x)
{
auto const *first{&Source[(static_cast<std::size_t>(y) * Width + horizontal.bounds[x].first) * 4]};
for (std::size_t channel = 0; channel < 3; ++channel)
{
temporary[(static_cast<std::size_t>(y) * TargetWidth + x) * 4 + channel] = mix(first + channel, along_row, x);
}
}
}
pass const along_column{vertical, static_cast<std::size_t>(TargetWidth) * 4};
for (auto y = 0; y < TargetHeight; ++y)
{
for (auto x = 0; x < TargetWidth; ++x)
{
auto const *first{&temporary[(static_cast<std::size_t>(vertical.bounds[y].first) * TargetWidth + x) * 4]};
auto *pixel{&Target[(static_cast<std::size_t>(y) * TargetWidth + x) * 4]};
for (std::size_t channel = 0; channel < 3; ++channel)
{
pixel[channel] = mix(first + channel, along_column, y);
}
pixel[3] = 255;
}
}
return true;
}
} // namespace wasm_screens

View File

@@ -0,0 +1,36 @@
/*
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/.
*/
#pragma once
#include <cstdint>
#include <span>
// image filters the wasm screens call through the host (eu07.led_post, eu07.resize_lanczos), run natively rather than
// in the interpreter. they give the very pixels of the Python screens: led_util.render_post() and Pillow's
// resize(..., Image.LANCZOS). the images are RGBA, 4 bytes a pixel, row by row; alpha is ignored and written as 255
namespace wasm_screens
{
// the size in bytes of an RGBA image
constexpr std::size_t image_size(int const Width, int const Height)
{
return static_cast<std::size_t>(Width) * static_cast<std::size_t>(Height) * 4;
}
// led_util's render_post: every diode becomes 2x2 pixels with a darker grid between them, glowing into its neighbours
// (Bloom), in a 1 pixel black frame. Target takes (2 Width + 2) x (2 Height + 2) pixels. returns false for sizes that
// don't fit the spans
bool led_post(std::span<std::uint8_t const> Source, int Width, int Height, std::span<std::uint8_t> Target, bool Bloom);
// Pillow's resize with the Lanczos filter, horizontally then vertically, each pass rounded to 8 bits. returns false
// for sizes that don't fit the spans
bool resize_lanczos(std::span<std::uint8_t const> Source, int Width, int Height, std::span<std::uint8_t> Target, int TargetWidth, int TargetHeight);
} // namespace wasm_screens

View File

@@ -0,0 +1,126 @@
/*
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/wasmscreenrules.h"
#include "utilities/parser.h"
#include <algorithm>
namespace wasm_screens
{
void screen_config::deserialize(cParser &Input)
{
while (deserialize_mapping(Input))
{
// all work done by while()
}
}
bool screen_config::deserialize_mapping(cParser &Input)
{
// token can be a key or block end
auto const key{Input.getToken<std::string>(true, "\n\r\t ,;[]")};
if (key.empty() || key == "}")
{
return false;
}
if (key == "{")
{
script = Input.getToken<std::string>();
}
else if (key == "target:")
{
target = Input.getToken<std::string>();
}
else if (key == "frames:")
{
frames = Input.getToken<int>();
}
else if (key == "parameters:")
{
parameters = Input.getToken<std::string>();
}
else if (key == "instance:")
{
instancing = Input.getToken<std::string>();
}
else if (key == "background:")
{
background = Input.getToken<std::string>();
}
else
{
// a key of a later version: skip its value
Input.getToken<std::string>();
}
return true;
}
frame_schedule::frame_schedule(std::uint64_t const Start, int const Interval, int const Phase)
: m_interval{std::max(Interval, 1)}, m_next{Start + static_cast<std::uint64_t>(std::max(Phase, 0))}
{
}
int frame_schedule::effective_interval(int const Configured, int const Requested)
{
return std::max({Configured, Requested, 1});
}
int frame_schedule::phase(std::string_view const Key, int const Interval)
{
if (Interval <= 1)
{
return 0;
}
// FNV-1a: the same offset for the same name on every compiler
std::uint32_t hash{2166136261u};
for (auto const character : Key)
{
hash ^= static_cast<std::uint8_t>(character);
hash *= 16777619u;
}
return static_cast<int>(hash % static_cast<std::uint32_t>(Interval));
}
bool frame_schedule::poll(std::uint64_t const Frame)
{
if (Frame < m_next)
{
return false;
}
m_next = Frame + static_cast<std::uint64_t>(m_interval);
return true;
}
bool is_safe_relative_path(std::string_view const Path)
{
if (Path.empty() || Path.front() == '/' || Path.front() == '\\' || Path.find(':') != std::string_view::npos)
{
return false;
}
std::size_t start{0};
while (start <= Path.size())
{
auto const end{std::min(Path.find_first_of("/\\", start), Path.size())};
if (Path.substr(start, end - start) == "..")
{
return false;
}
start = end + 1;
}
return true;
}
} // namespace wasm_screens

View File

@@ -0,0 +1,74 @@
/*
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/.
*/
#pragma once
#include <cstdint>
#include <string>
#include <string_view>
class cParser;
// the parts of the wasm screens that don't need the runtime: the .mmd entries, the frame schedule and the
// file access rules. kept apart from scripting/wasmscreens.cpp so the unit tests can check them
namespace wasm_screens
{
// a wasm screen as the .mmd file describes it:
// wasmscreen: { script.wasm target: submodel frames: 4 parameters: key=value&key=value }
// wasmdestinationsign: { script.wasm frames: 30 instance: name parameters: ... background: texture }
// every key but the script is optional, and an unknown key is skipped along with its value
struct screen_config
{
std::string script; // the .wasm file, relative to the vehicle folder when it has no path of its own
std::string target{"none"}; // cab screens: the submodel the screen is drawn on
std::string parameters; // extra key=value pairs passed to the screen with the simulation state
std::string instancing; // destination signs: "name" or "type", one sign per vehicle or per vehicle type
std::string background; // destination signs: the texture shown while the sign is off
int frames{0}; // the screen's own refresh interval in frames, used when longer than the one set in eu07.ini
void deserialize(cParser &Input);
private:
bool deserialize_mapping(cParser &Input);
};
// how often a screen gets fresh data, counted in rendered frames
class frame_schedule
{
public:
frame_schedule() = default;
// Start: the frame the screen was created on. the first refresh comes Phase frames later
frame_schedule(std::uint64_t Start, int Interval, int Phase);
// the interval of a screen: the one set in eu07.ini, or the screen's own one when that is longer. at least 1
static int effective_interval(int Configured, int Requested);
// a fixed offset in [0, Interval) taken from the screen's name, so that the screens sharing an interval
// don't all refresh on the same frame
static int phase(std::string_view Key, int Interval);
// true when the screen is due on Frame, and then not again until Interval frames later. a screen that
// wasn't asked for a while (a vehicle out of sight) is due on the first frame it is asked again
bool poll(std::uint64_t Frame);
int interval() const
{
return m_interval;
}
private:
int m_interval{1};
std::uint64_t m_next{0};
};
// true for a file a screen may read: a relative path inside the screen's folder, without '..', without a
// drive or a leading separator
bool is_safe_relative_path(std::string_view Path);
} // namespace wasm_screens

942
scripting/wasmscreens.cpp Normal file
View File

@@ -0,0 +1,942 @@
/*
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 <wasm_export.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <condition_variable>
#include <cstring>
#include <deque>
#include <format>
#include <fstream>
#include <iterator>
#include <limits>
#include <mutex>
#include <span>
#include <stop_token>
#include <string_view>
#include <thread>
#include <unordered_map>
#include <vector>
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<std::size_t>::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 <typename Sequence_> 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<std::string_view, entry> 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<std::uint8_t>(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<std::uint8_t>(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::int32_t>(std::clamp(Value, static_cast<double>(std::numeric_limits<std::int32_t>::min()), static_cast<double>(std::numeric_limits<std::int32_t>::max())));
}
std::int32_t to_int32(std::size_t const Value)
{
return static_cast<std::int32_t>(std::min<std::size_t>(Value, std::numeric_limits<std::int32_t>::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<bool> 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<pending_command> *commands{nullptr};
std::uint32_t vehicle{0};
// the image, handed from the worker to the main thread
std::mutex image_mutex;
std::vector<std::uint8_t> 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_screen *>(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<std::int32_t>(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<std::size_t>(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<std::size_t>(Index) >= points.size())
{
return 0;
}
auto *instance{wasm_runtime_get_module_inst(Env)};
std::array<float, 2> 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::size_t>(std::max<std::streamoff>(file.tellg(), 0))};
if (Buffer != nullptr && Capacity > 0)
{
file.seekg(0);
file.read(Buffer, static_cast<std::streamsize>(std::min<std::size_t>(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<double>(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<std::uint8_t> 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<std::uint8_t *>(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 <typename Function_> NativeSymbol native(char const *Name, Function_ *Function, char const *Signature)
{
return NativeSymbol{Name, reinterpret_cast<void *>(Function), Signature, nullptr};
}
} // namespace
struct wasm_screen_queue::implementation
{
struct module_entry
{
std::vector<std::uint8_t> bytes; // the runtime reads the module from here as long as it is loaded
wasm_module_t module{nullptr};
};
struct task
{
std::shared_ptr<wasm_screen> screen;
std::shared_ptr<dictionary_source> 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<NativeSymbol, 13> natives;
bool initialized{false};
std::atomic<std::uint64_t> frame{0};
std::mutex screens_mutex;
std::unordered_map<std::string, std::weak_ptr<wasm_screen>> screens;
std::mutex tasks_mutex;
std::condition_variable_any tasks_condition;
std::deque<task> tasks;
std::jthread worker;
std::mutex commands_mutex;
std::vector<pending_command> commands;
// the worker's, and the queue's once the worker stopped
std::unordered_map<std::string, module_entry> 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<pending_command> 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<std::uint32_t>(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<char, 256> error{};
Screen.instance = wasm_runtime_instantiate(module, stack_size, 0, error.data(), static_cast<std::uint32_t>(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<char>{file}, std::istreambuf_iterator<char>{});
if (entry.bytes.empty() || entry.bytes.size() > std::numeric_limits<std::uint32_t>::max())
{
ErrorLog(std::format("WASM screen \"{}\": invalid module size", Script));
return nullptr;
}
std::array<char, 256> error{};
entry.module = wasm_runtime_load(entry.bytes.data(), static_cast<std::uint32_t>(entry.bytes.size()), error.data(), static_cast<std::uint32_t>(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<std::uint32_t, 1> 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<std::int32_t>(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<std::size_t>(width) * static_cast<std::size_t>(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<std::uint8_t const *>(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<std::uint32_t>(command_target::vehicle) | Command.vehicle);
}
wasm_screen_queue::wasm_screen_queue() : m_impl{std::make_unique<implementation>()} {}
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<std::uint32_t>(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> 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<wasm_screen>(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<wasm_screen> const &Screen, std::shared_ptr<dictionary_source> 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<pending_command> sent;
{
std::scoped_lock lock{m_impl->commands_mutex};
sent.swap(m_impl->commands);
}
for (auto const &command : sent)
{
implementation::execute(command);
}
}

72
scripting/wasmscreens.h Normal file
View File

@@ -0,0 +1,72 @@
/*
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/.
*/
#pragma once
#include "scripting/wasmscreenrules.h"
#include <cstdint>
#include <memory>
#include <string>
struct ITexture;
struct dictionary_source;
// screens drawn by WebAssembly modules, the counterpart of the python screens (scripting/PyInt.h).
// a module gets the same state the python screens get, returns an RGBA image and may send commands. the
// interface between the two is described in wasm_screens_doc.md.
// unlike the python screens, which refresh after a time, a wasm screen refreshes every so many rendered
// frames: wasm.cabframes in eu07.ini for the screens in the cab, wasm.externalframes for the screens outside
// (destination signs)
enum class wasm_screen_kind
{
cab, // in the cab of the driven vehicle: may send commands to it
external // on the outside of a vehicle, such as a destination sign: shows only
};
// one screen: a module instance with its own memory, and the image it drew last
class wasm_screen;
class wasm_screen_queue
{
public:
struct screen_request
{
wasm_screen_kind kind{wasm_screen_kind::cab};
std::string script; // the .wasm file
std::string key; // requests with the same key get the same screen, e.g. the destination signs of one train
ITexture *texture{nullptr}; // where the screen is drawn; none for a screen that only sends commands
int frames{0}; // the screen's own refresh interval from the .mmd, used when longer than the eu07.ini one
};
wasm_screen_queue();
~wasm_screen_queue();
wasm_screen_queue(wasm_screen_queue const &) = delete;
wasm_screen_queue &operator=(wasm_screen_queue const &) = delete;
// starts the runtime and its worker thread. returns true on success
bool init();
// stops the worker and releases every screen, also the ones still held by vehicles
void exit();
// the screen for the request, made on the first request with its key. nullptr when the wasm screens are off
std::shared_ptr<wasm_screen> acquire(screen_request const &Request);
// true once per refresh interval of the screen. the caller then gathers the screen's input and submit()s it
bool due(wasm_screen &Screen) const;
// queues a refresh of the screen with the given input, replacing a refresh of it still waiting.
// Vehicle: the vehicle that gets the commands the screen sends; cab screens only
void submit(std::shared_ptr<wasm_screen> const &Screen, std::shared_ptr<dictionary_source> Input, std::uint32_t Vehicle);
// main thread, once per rendered frame: uploads the finished images, runs the commands the screens sent and
// counts the frame
void update();
private:
struct implementation;
std::unique_ptr<implementation> m_impl;
};

View File

@@ -0,0 +1,51 @@
/*
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/.
*/
// the wasm screens of a build without WITH_WASM: there are none, and the vehicles fall back to what they had before
#include "stdafx.h"
#include "scripting/wasmscreens.h"
struct wasm_screen_queue::implementation
{
};
wasm_screen_queue::wasm_screen_queue() : m_impl{std::make_unique<implementation>()} {}
wasm_screen_queue::~wasm_screen_queue() = default;
bool wasm_screen_queue::init()
{
return false;
}
void wasm_screen_queue::exit()
{
// nothing was started
}
std::shared_ptr<wasm_screen> wasm_screen_queue::acquire(screen_request const & /*Request*/)
{
return nullptr;
}
bool wasm_screen_queue::due(wasm_screen & /*Screen*/) const
{
return false;
}
void wasm_screen_queue::submit(std::shared_ptr<wasm_screen> const & /*Screen*/, std::shared_ptr<dictionary_source> /*Input*/, std::uint32_t /*Vehicle*/)
{
// nothing to refresh
}
void wasm_screen_queue::update()
{
// nothing to upload
}

View File

@@ -25,6 +25,11 @@ add_executable(${UNIT_TESTS_NAME}
# the core library brings the game's build settings along
target_link_libraries(${UNIT_TESTS_NAME} PRIVATE eu07_core)
# the wasm screen host, tested with the real runtime (test_wasm_host.cpp)
if (WITH_WASM)
target_sources(${UNIT_TESTS_NAME} PRIVATE "${CMAKE_SOURCE_DIR}/scripting/wasmscreens.cpp")
target_link_libraries(${UNIT_TESTS_NAME} PRIVATE eu07_wamr)
endif()
target_include_directories(${UNIT_TESTS_NAME} PRIVATE "${DEPS_DIR}/doctest")
# the names the tests CAPTURE are often C strings, shown as text rather than pointers
target_compile_definitions(${UNIT_TESTS_NAME} PRIVATE EU07_UNIT_TESTS_DATA="${CMAKE_CURRENT_SOURCE_DIR}/data" DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING)

View File

@@ -0,0 +1,288 @@
#include "stdafx.h"
#include "doctest.h"
// the wasm screen host (scripting/wasmscreens.cpp) with the real runtime: a module reads its input, draws an
// image that goes to the screen's texture and sends a command; a module stuck in a loop is stopped. built with
// WITH_WASM only
#ifdef WITH_WASM
#include "scripting/wasmscreens.h"
#include "input/command.h"
#include "interfaces/ITexture.h"
#include "utilities/Globals.h"
#include "utilities/dictionary.h"
#include <array>
#include <chrono>
#include <filesystem>
#include <format>
#include <fstream>
#include <functional>
#include <thread>
#include <vector>
// the engine's command queue and command names, which the test build doesn't link: the commands the screens send
// are kept here instead
namespace {
std::vector<std::pair<command_data, std::uint32_t>> &sent_commands() {
static std::vector<std::pair<command_data, std::uint32_t>> commands;
return commands;
}
} // namespace
namespace simulation {
command_queue Commands; // NOSONAR the engine's extern global, input/command.h
std::unordered_map<std::string, user_command> const commandMap{{"cmdtest", user_command::aidriverenable}}; // NOSONAR the engine's extern global, input/command.h
} // namespace simulation
void command_queue::push(command_data const &Command, uint32_t const Recipient) { // NOSONAR signature from input/command.h
sent_commands().emplace_back(Command, Recipient);
}
namespace {
// the modules, encoded by hand. "paint": a 2x2 screen in the colour 0xFF0000vv, vv being the input value
// "speed", which sends the command "cmdtest" with the parameters (vv, 7):
// (import "eu07" "get_float" (func $get_float (param i32 i32) (result f64)))
// (import "eu07" "command" (func $command (param i32 i32 i32 i32)))
// (memory (export "memory") 1)
// (data (i32.const 0) "speed") (data (i32.const 16) "cmdtest")
// (func (export "screen_width") (result i32) i32.const 2)
// (func (export "screen_height") (result i32) i32.const 2)
// (func (export "screen_render") (result i32) (local $v i32)
// (local.set $v (i32.trunc_f64_s (call $get_float (i32.const 0) (i32.const 5))))
// (i32.store (i32.const 1024) (i32.or (local.get $v) (i32.const 0xFF000000))) ... the same at 1028, 1032, 1036
// (call $command (i32.const 16) (i32.const 7) (local.get $v) (i32.const 7))
// (i32.const 1024))
constexpr std::array<std::uint8_t, 254> paint_module{
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x12, 0x03, 0x60, 0x02, 0x7f, 0x7f, 0x01,
0x7c, 0x60, 0x04, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x60, 0x00, 0x01, 0x7f, 0x02, 0x21, 0x02, 0x04,
0x65, 0x75, 0x30, 0x37, 0x09, 0x67, 0x65, 0x74, 0x5f, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x00, 0x00,
0x04, 0x65, 0x75, 0x30, 0x37, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x00, 0x01, 0x03,
0x04, 0x03, 0x02, 0x02, 0x02, 0x05, 0x03, 0x01, 0x00, 0x01, 0x07, 0x39, 0x04, 0x06, 0x6d, 0x65,
0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x0c, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x5f, 0x77, 0x69,
0x64, 0x74, 0x68, 0x00, 0x02, 0x0d, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x5f, 0x68, 0x65, 0x69,
0x67, 0x68, 0x74, 0x00, 0x03, 0x0d, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x5f, 0x72, 0x65, 0x6e,
0x64, 0x65, 0x72, 0x00, 0x04, 0x0a, 0x5e, 0x03, 0x04, 0x00, 0x41, 0x02, 0x0b, 0x04, 0x00, 0x41,
0x02, 0x0b, 0x52, 0x01, 0x01, 0x7f, 0x41, 0x00, 0x41, 0x05, 0x10, 0x00, 0xaa, 0x21, 0x00, 0x41,
0x80, 0x08, 0x20, 0x00, 0x41, 0x80, 0x80, 0x80, 0x78, 0x72, 0x36, 0x02, 0x00, 0x41, 0x84, 0x08,
0x20, 0x00, 0x41, 0x80, 0x80, 0x80, 0x78, 0x72, 0x36, 0x02, 0x00, 0x41, 0x88, 0x08, 0x20, 0x00,
0x41, 0x80, 0x80, 0x80, 0x78, 0x72, 0x36, 0x02, 0x00, 0x41, 0x8c, 0x08, 0x20, 0x00, 0x41, 0x80,
0x80, 0x80, 0x78, 0x72, 0x36, 0x02, 0x00, 0x41, 0x10, 0x41, 0x07, 0x20, 0x00, 0x41, 0x07, 0x10,
0x01, 0x41, 0x80, 0x08, 0x0b, 0x0b, 0x17, 0x02, 0x00, 0x41, 0x00, 0x0b, 0x05, 0x73, 0x70, 0x65,
0x65, 0x64, 0x00, 0x41, 0x10, 0x0b, 0x07, 0x63, 0x6d, 0x64, 0x74, 0x65, 0x73, 0x74};
// "spin": exports the same functions as "paint", but screen_render never returns: (loop $l (br $l))
constexpr std::array<std::uint8_t, 108> spin_module{
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03,
0x04, 0x03, 0x00, 0x00, 0x00, 0x05, 0x03, 0x01, 0x00, 0x01, 0x07, 0x39, 0x04, 0x06, 0x6d, 0x65,
0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x0c, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x5f, 0x77, 0x69,
0x64, 0x74, 0x68, 0x00, 0x00, 0x0d, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x5f, 0x68, 0x65, 0x69,
0x67, 0x68, 0x74, 0x00, 0x01, 0x0d, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x5f, 0x72, 0x65, 0x6e,
0x64, 0x65, 0x72, 0x00, 0x02, 0x0a, 0x15, 0x03, 0x04, 0x00, 0x41, 0x02, 0x0b, 0x04, 0x00, 0x41,
0x02, 0x0b, 0x09, 0x00, 0x03, 0x40, 0x0c, 0x00, 0x0b, 0x41, 0x00, 0x0b};
// a texture that keeps what was uploaded to it
struct recording_texture : ITexture {
bool create(bool) override { return true; }
int get_width() const override { return width; }
int get_height() const override { return height; }
size_t get_id() const override { return id; }
void release() override { /* nothing held */ }
void make_stub() override { /* nothing held */ }
std::string_view get_traits() const override { return ""; }
std::string_view get_name() const override { return "recording"; }
std::string_view get_type() const override { return "make:"; }
bool is_stub() const override { return false; }
bool get_has_alpha() const override { return true; }
bool get_is_ready() const override { return ready; }
void set_components_hint(int) override { /* not used */ }
void make_from_memory(size_t const Width, size_t const Height, const uint8_t *Data) override { update_from_memory(Width, Height, Data); }
void update_from_memory(size_t const Width, size_t const Height, const uint8_t *Data) override {
width = static_cast<int>(Width);
height = static_cast<int>(Height);
pixels.assign(Data, Data + Width * Height * 4);
++uploads;
}
bool ready{true};
size_t id{1};
int width{0};
int height{0};
int uploads{0};
std::vector<std::uint8_t> pixels;
};
// writes a module into a folder of its own, removed with the object
class module_file {
public:
template <std::size_t Size_>
module_file(std::string const &Name, std::array<std::uint8_t, Size_> const &Bytes)
: m_folder{std::filesystem::temp_directory_path() / std::format("eu07_wasm_test_{}", Name)} {
std::filesystem::create_directories(m_folder);
m_path = (m_folder / (Name + ".wasm")).generic_string();
std::ofstream file{m_path, std::ios::binary};
file.write(reinterpret_cast<char const *>(Bytes.data()), static_cast<std::streamsize>(Bytes.size()));
}
~module_file() {
std::error_code error;
std::filesystem::remove_all(m_folder, error);
}
module_file(module_file const &) = delete;
module_file &operator=(module_file const &) = delete;
std::string const &path() const { return m_path; }
private:
std::filesystem::path m_folder;
std::string m_path;
};
// runs frames until the condition holds, or gives up after a few seconds
bool run_until(wasm_screen_queue &Queue, std::function<bool()> const &Condition) {
auto const deadline{std::chrono::steady_clock::now() + std::chrono::seconds(10)};
while (std::chrono::steady_clock::now() < deadline) {
Queue.update();
if (Condition()) {
return true;
}
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
return false;
}
std::shared_ptr<dictionary_source> speed(double const Value) {
auto input{std::make_shared<dictionary_source>()};
input->insert("speed", Value);
return input;
}
} // namespace
TEST_CASE("wasm host: a cab screen draws from its input and sends commands to its vehicle") {
sent_commands().clear();
module_file const module{"paint", paint_module};
wasm_screen_queue queue;
REQUIRE(queue.init());
recording_texture texture;
auto const screen{queue.acquire({.kind = wasm_screen_kind::cab, .script = module.path(), .key = "cab", .texture = &texture, .frames = 0})};
REQUIRE(screen != nullptr);
// the same key, the same screen
CHECK(queue.acquire({.kind = wasm_screen_kind::cab, .script = module.path(), .key = "cab", .texture = &texture, .frames = 0}) == screen);
REQUIRE(run_until(queue, [&queue, &screen] { return queue.due(*screen); }));
queue.submit(screen, speed(5.0), 42);
REQUIRE(run_until(queue, [&texture] { return texture.uploads > 0 && false == sent_commands().empty(); }));
CHECK(texture.width == 2);
CHECK(texture.height == 2);
REQUIRE(texture.pixels.size() == 16);
std::array<std::uint8_t, 4> const expected{0x05, 0x00, 0x00, 0xFF};
for (std::size_t pixel = 0; pixel < 4; ++pixel) {
CAPTURE(pixel);
CHECK(std::equal(expected.begin(), expected.end(), texture.pixels.begin() + static_cast<std::ptrdiff_t>(pixel * 4)));
}
REQUIRE(sent_commands().size() == 1);
auto const &[command, recipient]{sent_commands().front()};
CHECK(command.command == user_command::aidriverenable);
CHECK(command.param1 == 5.0);
CHECK(command.param2 == 7.0);
CHECK(recipient == (static_cast<std::uint32_t>(command_target::vehicle) | 42u));
queue.exit();
}
TEST_CASE("wasm host: the image goes up again when the texture is made anew, and waits while it isn't there") {
sent_commands().clear();
module_file const module{"paint", paint_module};
wasm_screen_queue queue;
REQUIRE(queue.init());
recording_texture texture;
texture.ready = false;
auto const screen{queue.acquire({.kind = wasm_screen_kind::cab, .script = module.path(), .key = "cab", .texture = &texture, .frames = 0})};
REQUIRE(run_until(queue, [&queue, &screen] { return queue.due(*screen); }));
queue.submit(screen, speed(9.0), 1);
REQUIRE(run_until(queue, [] { return false == sent_commands().empty(); }));
queue.update();
CHECK(texture.uploads == 0);
texture.ready = true;
queue.update();
CHECK(texture.uploads == 1);
queue.update();
CHECK(texture.uploads == 1);
// released and made again: a new gl texture
texture.id = 2;
queue.update();
CHECK(texture.uploads == 2);
queue.exit();
}
TEST_CASE("wasm host: a screen outside the cab draws, but its commands are dropped") {
sent_commands().clear();
module_file const module{"paint", paint_module};
wasm_screen_queue queue;
REQUIRE(queue.init());
recording_texture texture;
auto const screen{queue.acquire({.kind = wasm_screen_kind::external, .script = module.path(), .key = "sign", .texture = &texture, .frames = 0})};
REQUIRE(run_until(queue, [&queue, &screen] { return queue.due(*screen); }));
queue.submit(screen, speed(3.0), 0);
REQUIRE(run_until(queue, [&texture] { return texture.uploads > 0; }));
queue.update();
CHECK(sent_commands().empty());
queue.exit();
}
TEST_CASE("wasm host: a screen stuck in a loop is stopped, and the others keep working") {
sent_commands().clear();
module_file const spinning{"spin", spin_module};
module_file const painting{"paint", paint_module};
wasm_screen_queue queue;
REQUIRE(queue.init());
recording_texture stuck;
recording_texture working;
auto const first{queue.acquire({.kind = wasm_screen_kind::cab, .script = spinning.path(), .key = "spin", .texture = &stuck, .frames = 0})};
auto const second{queue.acquire({.kind = wasm_screen_kind::cab, .script = painting.path(), .key = "paint", .texture = &working, .frames = 0})};
REQUIRE(run_until(queue, [&queue, &first] { return queue.due(*first); }));
REQUIRE(run_until(queue, [&queue, &second] { return queue.due(*second); }));
queue.submit(first, speed(1.0), 1);
queue.submit(second, speed(2.0), 1);
REQUIRE(run_until(queue, [&working] { return working.uploads > 0; }));
CHECK(stuck.uploads == 0);
// the stuck screen ran first, into the instruction limit, and isn't due any more
CHECK_FALSE(queue.due(*first));
queue.exit();
}
TEST_CASE("wasm host: the screens outlive the queue without harm") {
module_file const module{"paint", paint_module};
std::shared_ptr<wasm_screen> screen;
recording_texture texture;
{
wasm_screen_queue queue;
REQUIRE(queue.init());
screen = queue.acquire({.kind = wasm_screen_kind::cab, .script = module.path(), .key = "cab", .texture = &texture, .frames = 0});
REQUIRE(run_until(queue, [&queue, &screen] { return queue.due(*screen); }));
queue.submit(screen, speed(4.0), 1);
REQUIRE(run_until(queue, [&texture] { return texture.uploads > 0; }));
}
// the queue and the runtime are gone, the vehicle still holds its screen
CHECK(screen != nullptr);
screen.reset();
}
#endif

View File

@@ -0,0 +1,121 @@
#include "stdafx.h"
#include "doctest.h"
#include "scripting/wasmscreenkernels.h"
#include <array>
#include <span>
#include <vector>
// the image filters the wasm screens call through the host give the pixels of the Python screens: led_util's
// render_post() and Pillow's resize(..., Image.LANCZOS). the expected images were made by them (Pillow 12.2) from a
// 5x3 image of (x*53 + y*97, x*31 + y*17 + 100, x*7 + y*151 + 40) mod 256
namespace {
// RGB as Pillow gives it
constexpr std::array<std::uint8_t, 45> source{
0, 100, 40, 53, 131, 47, 106, 162, 54, 159, 193, 61, 212, 224, 68, 97, 117, 191, 150, 148, 198, 203, 179, 205,
0, 210, 212, 53, 241, 219, 194, 134, 86, 247, 165, 93, 44, 196, 100, 97, 227, 107, 150, 2, 114,
};
constexpr std::array<std::uint8_t, 288> posted{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 33, 144, 77, 28, 122, 65, 99, 194, 97,
84, 164, 82, 158, 236, 108, 134, 200, 91, 205, 255, 114, 174, 229, 96, 241, 255, 109, 204, 238, 92, 0, 0, 0,
0, 0, 0, 28, 122, 65, 23, 100, 53, 84, 164, 82, 69, 135, 67, 134, 200, 91, 110, 165, 75, 174, 229, 96,
143, 189, 79, 204, 238, 92, 168, 196, 76, 0, 0, 0, 0, 0, 0, 144, 173, 231, 122, 147, 196, 215, 229, 255,
182, 194, 219, 255, 255, 255, 219, 232, 230, 67, 255, 255, 56, 255, 235, 101, 255, 255, 85, 255, 225, 0, 0, 0,
0, 0, 0, 122, 147, 196, 100, 121, 161, 182, 194, 219, 150, 160, 180, 219, 232, 230, 180, 191, 189, 56, 255, 235,
46, 213, 193, 85, 255, 225, 70, 212, 185, 0, 0, 0, 0, 0, 0, 234, 178, 126, 198, 151, 107, 255, 231, 148,
251, 196, 125, 106, 255, 159, 90, 230, 135, 139, 255, 165, 118, 248, 140, 177, 65, 158, 150, 55, 134, 0, 0, 0,
0, 0, 0, 198, 151, 107, 163, 124, 88, 251, 196, 125, 207, 161, 103, 90, 230, 135, 74, 189, 111, 118, 248, 140,
97, 204, 115, 150, 55, 134, 123, 45, 110, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
constexpr std::array<std::uint8_t, 561> enlarged{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 12, 46, 26, 22, 90, 53, 18, 71, 41, 50, 102, 55, 58, 108, 58, 53, 98, 52,
88, 131, 65, 87, 128, 62, 84, 119, 58, 114, 144, 69, 102, 139, 63, 106, 133, 60, 136, 144, 67, 139, 144, 68,
56, 71, 29, 0, 0, 0, 0, 0, 0, 16, 85, 39, 25, 165, 76, 21, 130, 59, 80, 188, 84, 94, 198, 89,
86, 178, 78, 155, 246, 102, 150, 236, 97, 151, 221, 89, 232, 255, 112, 210, 255, 100, 213, 250, 94, 255, 255, 108,
255, 255, 107, 113, 129, 44, 0, 0, 0, 0, 0, 0, 19, 67, 38, 31, 130, 74, 25, 100, 57, 72, 148, 78,
83, 155, 82, 75, 137, 72, 128, 191, 92, 124, 181, 87, 119, 172, 81, 167, 225, 99, 146, 202, 89, 151, 197, 85,
200, 238, 97, 198, 235, 95, 79, 96, 39, 0, 0, 0, 0, 0, 0, 70, 90, 108, 135, 177, 217, 108, 140, 171,
172, 198, 208, 188, 211, 215, 167, 186, 192, 218, 235, 219, 227, 238, 214, 175, 222, 204, 95, 245, 222, 62, 245, 213,
89, 240, 209, 122, 244, 224, 123, 243, 224, 49, 124, 99, 0, 0, 0, 0, 0, 0, 74, 89, 130, 139, 174, 255,
110, 136, 201, 182, 194, 246, 197, 205, 253, 170, 182, 223, 244, 239, 255, 255, 234, 252, 186, 218, 236, 52, 255, 255,
15, 245, 249, 49, 244, 240, 72, 255, 255, 73, 255, 255, 29, 130, 113, 0, 0, 0, 0, 0, 0, 76, 82, 96,
147, 160, 188, 114, 123, 145, 166, 177, 183, 181, 187, 188, 163, 165, 163, 186, 224, 205, 188, 214, 191, 140, 200, 176,
80, 251, 215, 54, 228, 190, 75, 211, 179, 105, 219, 206, 106, 217, 204, 42, 87, 82, 0, 0, 0, 0, 0, 0,
124, 96, 69, 250, 189, 137, 195, 149, 108, 225, 207, 138, 250, 221, 143, 237, 195, 127, 134, 244, 156, 80, 245, 148,
103, 228, 139, 135, 248, 165, 120, 252, 150, 129, 193, 143, 175, 68, 160, 178, 59, 161, 72, 34, 66, 0, 0, 0,
0, 0, 0, 135, 101, 67, 255, 197, 133, 206, 153, 103, 255, 216, 134, 255, 228, 139, 247, 200, 122, 127, 255, 152,
67, 255, 144, 98, 237, 134, 147, 255, 162, 131, 255, 146, 138, 187, 138, 189, 48, 157, 190, 37, 156, 76, 26, 64,
0, 0, 0, 0, 0, 0, 57, 44, 32, 110, 85, 64, 84, 65, 48, 121, 94, 63, 140, 98, 65, 102, 86, 57,
60, 120, 72, 39, 112, 68, 46, 103, 62, 61, 134, 76, 52, 133, 68, 56, 82, 64, 78, 34, 74, 77, 29, 73,
31, 16, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
};
constexpr std::array<std::uint8_t, 105> reduced{
2, 22, 10, 14, 58, 25, 35, 70, 30, 53, 83, 33, 74, 90, 34, 95, 99, 36, 37, 40, 13, 17, 60, 41,
63, 155, 101, 115, 186, 110, 157, 217, 117, 161, 234, 120, 207, 255, 127, 81, 103, 47, 52, 64, 91, 142, 165, 218,
186, 193, 220, 217, 219, 227, 58, 229, 227, 70, 251, 241, 32, 101, 90, 92, 72, 57, 230, 184, 137, 233, 214, 144,
110, 238, 155, 114, 255, 157, 158, 131, 166, 64, 23, 60, 32, 24, 16, 84, 60, 38, 82, 70, 41, 27, 79, 44,
40, 90, 45, 56, 29, 47, 22, 2, 17,
};
constexpr std::array<std::uint8_t, 288> plain{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100, 40, 0, 85, 34, 53, 131, 47,
45, 111, 39, 106, 162, 54, 90, 137, 45, 159, 193, 61, 135, 164, 51, 212, 224, 68, 180, 190, 57, 0, 0, 0,
0, 0, 0, 0, 85, 34, 0, 70, 28, 45, 111, 39, 37, 91, 32, 90, 137, 45, 74, 113, 37, 135, 164, 51,
111, 135, 42, 180, 190, 57, 148, 156, 47, 0, 0, 0, 0, 0, 0, 97, 117, 191, 82, 99, 162, 150, 148, 198,
127, 125, 168, 203, 179, 205, 172, 152, 174, 0, 210, 212, 0, 178, 180, 53, 241, 219, 45, 204, 186, 0, 0, 0,
0, 0, 0, 82, 99, 162, 67, 81, 133, 127, 125, 168, 105, 103, 138, 172, 152, 174, 142, 125, 143, 0, 178, 180,
0, 147, 148, 45, 204, 186, 37, 168, 153, 0, 0, 0, 0, 0, 0, 194, 134, 86, 164, 113, 73, 247, 165, 93,
209, 140, 79, 44, 196, 100, 37, 166, 85, 97, 227, 107, 82, 192, 90, 150, 2, 114, 127, 1, 96, 0, 0, 0,
0, 0, 0, 164, 113, 73, 135, 93, 60, 209, 140, 79, 172, 115, 65, 37, 166, 85, 30, 137, 70, 82, 192, 90,
67, 158, 74, 127, 1, 96, 105, 1, 79, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
std::vector<std::uint8_t> rgba(std::span<std::uint8_t const> const Rgb) {
std::vector<std::uint8_t> result;
for (std::size_t idx = 0; idx < Rgb.size(); idx += 3) {
result.insert(result.end(), {Rgb[idx], Rgb[idx + 1], Rgb[idx + 2], 255});
}
return result;
}
} // namespace
TEST_CASE("wasm kernels: led_post gives the pixels of led_util.render_post") {
auto const input{rgba(source)};
std::vector<std::uint8_t> output(wasm_screens::image_size(12, 8), 7);
REQUIRE(wasm_screens::led_post(input, 5, 3, output, true));
CHECK(output == rgba(posted));
REQUIRE(wasm_screens::led_post(input, 5, 3, output, false));
CHECK(output == rgba(plain));
}
TEST_CASE("wasm kernels: resize_lanczos gives the pixels of Pillow's resize with LANCZOS") {
auto const input{rgba(posted)};
std::vector<std::uint8_t> larger(wasm_screens::image_size(17, 11));
REQUIRE(wasm_screens::resize_lanczos(input, 12, 8, larger, 17, 11));
CHECK(larger == rgba(enlarged));
std::vector<std::uint8_t> smaller(wasm_screens::image_size(7, 5));
REQUIRE(wasm_screens::resize_lanczos(input, 12, 8, smaller, 7, 5));
CHECK(smaller == rgba(reduced));
}
TEST_CASE("wasm kernels: images that don't fit their buffers are refused") {
std::vector<std::uint8_t> const input(wasm_screens::image_size(5, 3));
std::vector<std::uint8_t> output(wasm_screens::image_size(12, 8) - 1);
CHECK_FALSE(wasm_screens::led_post(input, 5, 3, output, true));
CHECK_FALSE(wasm_screens::led_post(input, 0, 3, output, true));
CHECK_FALSE(wasm_screens::resize_lanczos(input, 5, 3, output, 12, 8));
CHECK_FALSE(wasm_screens::resize_lanczos(input, 6, 3, output, 2, 2));
}

View File

@@ -0,0 +1,103 @@
#include "stdafx.h"
#include "doctest.h"
#include "scripting/wasmscreenrules.h"
#include "utilities/parser.h"
// the parts of the wasm screens that don't need the runtime: the .mmd entries, the refresh schedule counted in
// frames and the files a screen may read
using wasm_screens::frame_schedule;
using wasm_screens::screen_config;
TEST_CASE("wasm screens: a cab screen entry with every key") {
cParser parser("{ ekran.wasm target: screen01 frames: 6 parameters: id=1&mode=full } next:", cParser::buffer_TEXT);
screen_config config;
config.deserialize(parser);
CHECK(config.script == "ekran.wasm");
CHECK(config.target == "screen01");
CHECK(config.frames == 6);
CHECK(config.parameters == "id=1&mode=full");
// the parser stops at the end of the block
CHECK(parser.getToken<std::string>() == "next:");
}
TEST_CASE("wasm screens: the optional keys keep their defaults, an unknown key is skipped with its value") {
cParser parser("{ tablica.wasm later: 12 instance: name background: tablica_off }", cParser::buffer_TEXT);
screen_config config;
config.deserialize(parser);
CHECK(config.script == "tablica.wasm");
CHECK(config.target == "none");
CHECK(config.frames == 0);
CHECK(config.parameters.empty());
CHECK(config.instancing == "name");
CHECK(config.background == "tablica_off");
}
TEST_CASE("wasm screens: the interval set in eu07.ini, or the screen's own when it is longer") {
CHECK(frame_schedule::effective_interval(4, 0) == 4);
CHECK(frame_schedule::effective_interval(4, 10) == 10);
CHECK(frame_schedule::effective_interval(4, 2) == 4);
// never less than every frame
CHECK(frame_schedule::effective_interval(0, 0) == 1);
CHECK(frame_schedule::effective_interval(-3, -1) == 1);
}
TEST_CASE("wasm screens: a screen is due once per interval") {
frame_schedule schedule{100, 3, 0};
CHECK(schedule.poll(100));
// asked again in the same frame (the vehicle drawn in more than one pass)
CHECK_FALSE(schedule.poll(100));
CHECK_FALSE(schedule.poll(101));
CHECK_FALSE(schedule.poll(102));
CHECK(schedule.poll(103));
CHECK_FALSE(schedule.poll(105));
CHECK(schedule.poll(106));
}
TEST_CASE("wasm screens: a screen out of sight for a while is due on the first frame it is asked again") {
frame_schedule schedule{0, 5, 0};
CHECK(schedule.poll(0));
CHECK(schedule.poll(42));
CHECK_FALSE(schedule.poll(46));
CHECK(schedule.poll(47));
}
TEST_CASE("wasm screens: the phase spreads the screens over the frames of an interval") {
auto const interval{30};
auto const phase{frame_schedule::phase("make:dynamic/pkp/ed72/tablica.wasm?$timetable=ed72-001", interval)};
CHECK(phase >= 0);
CHECK(phase < interval);
// the same name always gets the same phase
CHECK(phase == frame_schedule::phase("make:dynamic/pkp/ed72/tablica.wasm?$timetable=ed72-001", interval));
// a screen refreshed every frame has no phase
CHECK(frame_schedule::phase("anything", 1) == 0);
frame_schedule schedule{1000, interval, phase};
auto first{std::uint64_t{0}};
for (auto frame = std::uint64_t{1000}; frame < 1000 + interval; ++frame) {
if (schedule.poll(frame)) {
first = frame;
break;
}
}
CHECK(first == 1000 + static_cast<std::uint64_t>(phase));
}
TEST_CASE("wasm screens: a screen reads files only below its own folder") {
CHECK(wasm_screens::is_safe_relative_path("font.png"));
CHECK(wasm_screens::is_safe_relative_path("data/font.png"));
CHECK(wasm_screens::is_safe_relative_path("data\\font.png"));
CHECK(wasm_screens::is_safe_relative_path("data/..font.png"));
CHECK_FALSE(wasm_screens::is_safe_relative_path(""));
CHECK_FALSE(wasm_screens::is_safe_relative_path("../eu07.ini"));
CHECK_FALSE(wasm_screens::is_safe_relative_path("data/../../eu07.ini"));
CHECK_FALSE(wasm_screens::is_safe_relative_path("data\\..\\x"));
CHECK_FALSE(wasm_screens::is_safe_relative_path(".."));
CHECK_FALSE(wasm_screens::is_safe_relative_path("/etc/passwd"));
CHECK_FALSE(wasm_screens::is_safe_relative_path("\\windows"));
CHECK_FALSE(wasm_screens::is_safe_relative_path("c:/windows/win.ini"));
}

View File

@@ -72,6 +72,10 @@ void global_settings::FinalizeConfig()
python_enabled = false;
#endif
#ifndef WITH_WASM
wasm_enabled = false;
#endif
#ifdef _WIN32
Console::ModeSet(iFeedbackMode, iFeedbackPort);
#endif
@@ -877,6 +881,29 @@ bool global_settings::ConfigParseUI(cParser& Parser, const std::string& token)
return false;
}
bool global_settings::ConfigParseWasm(cParser& Parser, std::string_view const token)
{
if (token == "wasm.enabled")
{
ParseOne(Parser, wasm_enabled, 1);
return true;
}
if (token == "wasm.cabframes")
{
ParseOneClamped(Parser, wasm_cabframes, 1, 1000);
return true;
}
if (token == "wasm.externalframes")
{
ParseOneClamped(Parser, wasm_externalframes, 1, 1000);
return true;
}
return false;
}
bool global_settings::ConfigParsePython(cParser& Parser, const std::string& token)
{
if (token == "python.enabled")
@@ -1373,6 +1400,9 @@ void global_settings::ConfigParse(cParser& Parser)
if (ConfigParsePython(Parser, token))
continue;
if (ConfigParseWasm(Parser, token))
continue;
if (ConfigParseNetwork(Parser, token))
continue;
@@ -1840,6 +1870,9 @@ global_settings::export_as_text( std::ostream &Output ) const {
export_as_text( Output, "python.threadedupload", python_threadedupload );
export_as_text( Output, "python.uploadmain", python_uploadmain );
export_as_text( Output, "python.mipmaps", python_mipmaps );
export_as_text( Output, "wasm.enabled", wasm_enabled );
export_as_text( Output, "wasm.cabframes", wasm_cabframes );
export_as_text( Output, "wasm.externalframes", wasm_externalframes );
export_as_text( Output, "async.trainThreads", trainThreads );
for( auto const &server : network_servers ) {
Output

View File

@@ -296,6 +296,9 @@ struct global_settings {
bool python_sharectx = true;
bool python_uploadmain = true;
std::chrono::duration<float> python_minframetime {0.01f};
bool wasm_enabled{true}; // screens drawn by WebAssembly modules (wasmscreen:, wasmdestinationsign: in the .mmd)
int wasm_cabframes{4}; // the wasm screens in the cab are refreshed every so many rendered frames
int wasm_externalframes{30}; // the wasm screens outside the cab (destination signs) are refreshed every so many rendered frames
bool gfx_skiprendering = false;
int gfx_framebuffer_width = -1;
@@ -390,6 +393,7 @@ struct global_settings {
bool ConfigParseSimulation(cParser& Parser, const std::string& token);
bool ConfigParseUI(cParser& Parser, const std::string& token);
bool ConfigParsePython(cParser& Parser, const std::string& token);
bool ConfigParseWasm(cParser& Parser, std::string_view token);
bool ConfigParseNetwork(cParser& Parser, const std::string& token);
bool ConfigParseHardware(cParser& Parser, const std::string& token);
bool ConfigParseDebug(cParser& Parser, const std::string& token);

View File

@@ -35,6 +35,7 @@ http://mozilla.org/MPL/2.0/.
#include "rendering/renderer.h"
#include "application/uitranscripts.h"
#include "input/messaging.h"
#include "application/application.h"
#include "vehicle/Driver.h"
// Ra: taki zapis funkcjonuje lepiej, ale może nie jest optymalny
@@ -3139,6 +3140,20 @@ void TDynamicObject::update_destinations() {
&& ctOwner != nullptr)) ? // lcd signs are off for carriages without engine, potentially left on a siding
DestinationSign.destination :
DestinationSign.destination_off;
update_wasm_destination();
}
void TDynamicObject::update_wasm_destination() {
if( DestinationSign.wasm_sign == nullptr ) { return; }
// only while the sign shows the destination it draws
if( m_materialdata.replacable_skins[ 4 ] != DestinationSign.destination ) { return; }
auto &queue { Application.wasm_screens() };
if( false == queue.due( *DestinationSign.wasm_sign ) ) { return; }
// read anew on every refresh, so the sign follows the timetable
queue.submit( DestinationSign.wasm_sign, std::make_shared<dictionary_source>( DestinationSign.wasm_request ), 0 );
}
bool TDynamicObject::Update(double dt, double dt1)
@@ -7080,6 +7095,14 @@ void TDynamicObject::LoadMMediaFile( std::string const &TypeName, std::string co
DestinationSign.script = asBaseDir + DestinationSign.script;
}
}
else if( token == "wasmdestinationsign:" ) {
DestinationSign.wasm.deserialize( parser );
// supply vehicle folder as script path if none is provided
if( false == DestinationSign.wasm.script.empty()
&& substr_path( DestinationSign.wasm.script ).empty() ) {
DestinationSign.wasm.script = asBaseDir + DestinationSign.wasm.script;
}
}
// NOTE: legacy key, now expected as optional "background:" parameter in pydestinationsign: { parameter block }
else if( token == "destinationsignbackground:" ) {
parser.getTokens();
@@ -7099,7 +7122,9 @@ void TDynamicObject::LoadMMediaFile( std::string const &TypeName, std::string co
if( ReplacableSkin != "none" ) {
// potentially set blank destination texture
DestinationSign.destination_off = DestinationFind( DestinationSign.background.empty() ? "nowhere" : DestinationSign.background );
auto const wasmbackground { Global.wasm_enabled && false == DestinationSign.wasm.script.empty() && false == DestinationSign.wasm.background.empty() };
auto const &background { wasmbackground ? DestinationSign.wasm.background : DestinationSign.background };
DestinationSign.destination_off = DestinationFind( background.empty() ? "nowhere" : background );
}
// assign default samples to sound emitters which weren't included in the config file
@@ -7920,6 +7945,7 @@ void TDynamicObject::DestinationClear() {
if( false == DestinationSign.has_light ) { return; } // physical destination signs remain up until manually changed
// update_destinations() shows the blank sign in place of the missing destination
DestinationSign.destination = null_handle;
DestinationSign.wasm_sign.reset();
}
void TDynamicObject::DestinationSet(std::string const &to, std::string const &numer) {
@@ -7933,6 +7959,8 @@ void TDynamicObject::DestinationSet(std::string const &to, std::string const &nu
if( std::abs( m_materialdata.multi_textures ) >= 4 ) { return; } // jak są 4 tekstury wymienne, to nie zmieniać rozkładem
if( DestinationSign.sign == nullptr ) { return; } // no sign submodel, no problem
// a sign drawn for the previous destination
DestinationSign.wasm_sign.reset();
// now see if we can find any version of the destination texture
std::vector<std::string> const destinations = {
@@ -7948,21 +7976,27 @@ void TDynamicObject::DestinationSet(std::string const &to, std::string const &nu
}
}
// if we didn't get static texture we might be able to make one
if( true == destination_set_wasm( numer ) ) { return; }
if( DestinationSign.script.empty() ) { return; } // no script so no way to make the texture
if( numer == "none" ) { return; } // blank or incomplete/malformed timetable, don't bother
DestinationSign.destination = GfxRenderer->Fetch_Material( destination_request( DestinationSign.script, DestinationSign.instancing, DestinationSign.parameters ) );
}
std::string TDynamicObject::destination_request( std::string const &Script, std::string const &Instancing, std::string const &Parameters ) const {
auto const trainname { [this]() -> std::string {
if( ctOwner != nullptr ) { return ctOwner->TrainName(); }
if( Mechanik != nullptr ) { return Mechanik->TrainName(); }
return "none"; } }; // shouldn't get here but, eh
auto const instance { [this]() -> std::string {
if( DestinationSign.instancing == "name" ) { return MoverParameters->Name; }
if( DestinationSign.instancing == "type" ) { return MoverParameters->TypeName; }
auto const instance { [this, &Instancing]() -> std::string {
if( Instancing == "name" ) { return MoverParameters->Name; }
if( Instancing == "type" ) { return MoverParameters->TypeName; }
return "none"; } };
std::string signrequest {
"make:"
+ DestinationSign.script
+ Script
+ "?"
// timetable include
+ "$timetable=" + (
@@ -7973,15 +8007,40 @@ void TDynamicObject::DestinationSet(std::string const &to, std::string const &nu
// NOTE: underscore doesn't have any magic meaning for the time being, it's just less likely to conflict with regular dictionary keys
+ "&_id1=" + trainname() };
// TBD, TODO: replace instancing with support for variables in extra parameters string?
if( false == DestinationSign.instancing.empty() ) {
if( false == Instancing.empty() ) {
signrequest += "&_id2=" + instance();
}
// optionl extra parameters
if( false == DestinationSign.parameters.empty() ) {
signrequest += "&" + DestinationSign.parameters;
if( false == Parameters.empty() ) {
signrequest += "&" + Parameters;
}
return signrequest;
}
DestinationSign.destination = GfxRenderer->Fetch_Material( signrequest );
bool TDynamicObject::destination_set_wasm( std::string const &Numer ) {
if( false == Global.wasm_enabled ) { return false; }
if( DestinationSign.wasm.script.empty() ) { return false; } // no wasm sign, maybe a python one
if( Numer == "none" ) { return false; } // blank or incomplete/malformed timetable, don't bother
auto const signrequest { destination_request( DestinationSign.wasm.script, DestinationSign.wasm.instancing, DestinationSign.wasm.parameters ) };
auto const signmaterial { GfxRenderer->Fetch_Material( signrequest ) };
if( signmaterial == null_handle ) { return false; }
auto &signtexture { GfxRenderer->Texture( GfxRenderer->Material( signmaterial )->GetTexture( 0 ) ) };
// vehicles showing the same request (one train, one instance) share the screen, as they share the texture
auto sign { Application.wasm_screens().acquire( {
.kind = wasm_screen_kind::external,
.script = DestinationSign.wasm.script,
.key = signrequest,
.texture = &signtexture,
.frames = DestinationSign.wasm.frames } ) };
if( sign == nullptr ) { return false; } // the wasm screens are off
DestinationSign.destination = signmaterial;
DestinationSign.wasm_sign = std::move( sign );
DestinationSign.wasm_request = signrequest.substr( signrequest.find( '?' ) + 1 );
return true;
}
material_handle TDynamicObject::DestinationFind( std::string Destination ) {

View File

@@ -22,8 +22,12 @@ http://mozilla.org/MPL/2.0/.
#include "audio/sound.h"
#include "world/Spring.h"
#include "vehicle/mirrors.h"
#include "scripting/wasmscreenrules.h"
#include <vector>
#include <memory>
class wasm_screen; // scripting/wasmscreens.h
#define EU07_SOUND_BOGIESOUNDS
@@ -251,6 +255,9 @@ public:
int update_rate { 0 }; // -1: per stop, 0: none, >0: fps // TBD, TODO: implement?
std::string instancing; // potential method to generate more than one texture per timetable
std::string parameters; // potential extra parameters supplied by mmd file
wasm_screens::screen_config wasm; // potential WebAssembly sign (wasmdestinationsign:), used in place of the python one while wasm screens are on
std::shared_ptr<wasm_screen> wasm_sign; // the wasm screen drawing the current destination
std::string wasm_request; // its input: the key=value pairs a python sign gets for its texture
// methods
void deserialize( cParser &Input );
private:
@@ -726,6 +733,8 @@ private:
void update_load_offset();
void shuffle_load_order();
void update_destinations();
// feeds the wasm destination sign with fresh data, when it is shown and due
void update_wasm_destination();
bool Update(double dt, double dt1);
bool FastUpdate(double dt);
void Move(double fDistance);
@@ -849,6 +858,10 @@ private:
// blanks electronic destination sign at the end of the service; physical signs stay until changed
void DestinationClear();
material_handle DestinationFind( std::string Destination );
// the name of a generated destination sign texture: the script, the timetable, the instancing and the parameters
std::string destination_request( std::string const &Script, std::string const &Instancing, std::string const &Parameters ) const;
// makes the destination sign a wasm screen, when the vehicle has one and the wasm screens are on. returns true when it did
bool destination_set_wasm( std::string const &Numer );
void OverheadTrack(float o);
glm::dvec3 get_future_movement() const;
void move_set(double distance);

View File

@@ -38,6 +38,7 @@ http://mozilla.org/MPL/2.0/.
#include <thread>
#include <cmath>
#include <algorithm>
#include <format>
/*
namespace input {
@@ -10035,7 +10036,10 @@ bool TTrain::Update(double const Deltatime)
// screens
if (!FreeFlyModeFlag && simulation::Train == this) // don't bother if we're outside
{
update_screens(Deltatime);
update_wasm_screens();
}
// update direction relay
if (prevBatState != mvOccupied->Power24vIsAvailable)
@@ -10564,6 +10568,92 @@ void TTrain::update_screens(double dt)
}
}
void TTrain::update_wasm_screens()
{
auto &queue{Application.wasm_screens()};
for (auto &entry : m_wasmscreens)
{
if (false == queue.due(*entry.screen))
{
continue;
}
auto input{GetTrainState(entry.parameters)};
input->insert("touches", *entry.touch_list);
entry.touch_list->clear();
queue.submit(entry.screen, std::move(input), id());
}
}
bool TTrain::find_screen_surface(std::string const &Target, std::string_view const Kind, TSubModel *&Submodel, ITexture *&Texture) const
{
Submodel = DynamicObject->mdKabina ? DynamicObject->mdKabina->GetFromName(Target) :
DynamicObject->mdLowPolyInt ? DynamicObject->mdLowPolyInt->GetFromName(Target) :
nullptr;
if (Submodel == nullptr)
{
WriteLog(std::format("{}: submodel {} not found - Ignoring screen", Kind, Target));
return false;
}
auto const material{Submodel->GetMaterial()};
if (material <= 0)
{
// sub model nie posiada tekstury lub tekstura wymienna - nie obslugiwana
WriteLog(std::format("{}: invalid texture id {} - Ignoring screen", Kind, material));
return false;
}
Texture = &GfxRenderer->Texture(GfxRenderer->Material(material)->GetTexture(0));
return true;
}
void TTrain::load_wasm_screen(cParser &Parser)
{
wasm_screen_entry entry;
// the block is read whole even when the wasm screens are off
entry.config.deserialize(Parser);
if (entry.config.script.empty())
{
return;
}
if (substr_path(entry.config.script).empty())
{
entry.config.script = DynamicObject->asBaseDir + entry.config.script;
}
TSubModel *submodel{nullptr};
ITexture *texture{nullptr};
if (entry.config.target != "none" && false == find_screen_surface(entry.config.target, "WASM Screen", submodel, texture))
{
return;
}
entry.screen = Application.wasm_screens().acquire({.kind = wasm_screen_kind::cab,
.script = entry.config.script,
.key = std::format("cab:{}:{}:{}", DynamicObject->name(), entry.config.target, entry.config.script),
.texture = texture,
.frames = entry.config.frames});
if (entry.screen == nullptr)
{
// the wasm screens are off
return;
}
if (texture != nullptr)
{
// make the surface static so it doesn't get destroyed by garbage collector if the user spends long time outside cab
texture->create(true);
}
if (false == entry.config.parameters.empty())
{
entry.parameters = dictionary_source(entry.config.parameters);
}
entry.touch_list = std::make_shared<std::vector<glm::vec2>>();
if (submodel != nullptr)
{
submodel->screen_touch_list = entry.touch_list;
}
m_wasmscreens.emplace_back(std::move(entry));
}
void TTrain::add_distance(double const Distance)
{
@@ -10739,6 +10829,7 @@ bool TTrain::InitializeCab(int NewCabNo, std::string const &asFileName)
m_controlmapper.clear();
// clear python screens
m_screens.clear();
m_wasmscreens.clear();
// reset sound positions
auto const nullvector{glm::vec3()};
std::vector<std::reference_wrapper<std::optional<sound_source>>> sounds = {
@@ -10930,23 +11021,10 @@ bool TTrain::InitializeCab(int NewCabNo, std::string const &asFileName)
TSubModel *submodel = nullptr;
if (screen.target != "none")
{
submodel = DynamicObject->mdKabina ? DynamicObject->mdKabina->GetFromName(screen.target) :
DynamicObject->mdLowPolyInt ? DynamicObject->mdLowPolyInt->GetFromName(screen.target) :
nullptr;
if (submodel == nullptr)
if (false == find_screen_surface(screen.target, "Python Screen", submodel, tex))
{
WriteLog("Python Screen: submodel " + screen.target + " not found - Ignoring screen");
continue;
}
auto const material{submodel->GetMaterial()};
if (material <= 0)
{
// sub model nie posiada tekstury lub tekstura wymienna - nie obslugiwana
WriteLog("Python Screen: invalid texture id " + std::to_string(material) + " - Ignoring screen");
continue;
}
tex = &GfxRenderer->Texture(GfxRenderer->Material(material)->GetTexture(0));
}
else
{
@@ -10973,6 +11051,10 @@ bool TTrain::InitializeCab(int NewCabNo, std::string const &asFileName)
if (Global.python_displaywindows)
m_screens.back().viewer = std::make_unique<python_screen_viewer>(rt, m_screens.back().touch_list, m_screens.back().script);
}
else if (token == "wasmscreen:")
{
load_wasm_screen(parser);
}
else if (token == "pyscreenupdatetime:")
{
parser.getTokens();

View File

@@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/.
#include "scripting/PyInt.h"
#include "input/command.h"
#include "scripting/pythonscreenviewer.h"
#include "scripting/wasmscreens.h"
#include "utilities/dictionary.h"
#undef snprintf // pyint.h->python
@@ -145,6 +146,14 @@ class TTrain {
using screenentry_sequence = std::vector<screen_entry>;
// a screen of the cab drawn by a WebAssembly module (wasmscreen: in the .mmd)
struct wasm_screen_entry {
wasm_screens::screen_config config;
dictionary_source parameters; // the config's parameters, pre-processed
std::shared_ptr<wasm_screen> screen;
std::shared_ptr<std::vector<glm::vec2>> touch_list;
};
// methods
bool CabChange(int iDirection);
bool ShowNextCurrent{false}; // pokaz przd w podlaczonej lokomotywie (ET41)
@@ -233,6 +242,10 @@ class TTrain {
end cab_to_end() const {
return cab_to_end( iCabn ); }
void update_screens(double dt);
void update_wasm_screens();
// finds the submodel and the texture a cab screen is drawn on. returns false when the screen is to be ignored
bool find_screen_surface(std::string const &Target, std::string_view Kind, TSubModel *&Submodel, ITexture *&Texture) const;
void load_wasm_screen(cParser &Parser);
// command handlers
// NOTE: we're currently using universal handlers and static handler map but it may be beneficial to have these implemented on individual class instance basis
@@ -935,6 +948,7 @@ private:
bool m_mastercontrollerinuse { false };
float m_mastercontrollerreturndelay { 0.f };
screenentry_sequence m_screens;
std::vector<wasm_screen_entry> m_wasmscreens;
uint16_t vid { 0 }; // train network recipient id
float m_distancecounter { -1.f }; // distance traveled since meter was activated or -1 if inactive
double m_brakehandlecp{ 0.0 };

129
wasm_screens_doc.md Normal file
View File

@@ -0,0 +1,129 @@
# WebAssembly screens
Screens drawn by WebAssembly modules, alongside the Python screens (`pyscreen:`, `pydestinationsign:`). A module
gets the same input a Python screen gets (the table in `python_var_doc.md`), returns an RGBA image and, in the cab,
may send commands. The modules run in the WAMR interpreter (`ref/wamr`), in a sandbox: a module reaches nothing but
the functions of the `eu07` module below, and reads files only from its own folder.
Unlike a Python screen, which refreshes after a time (`python.updatetime`, `updatetime:`), a WebAssembly screen
refreshes every so many rendered frames, set in `eu07.ini`.
## eu07.ini
| key | default | meaning |
|-----------------------|---------|--------------------------------------------------------------------------------|
| `wasm.enabled` | 1 | 0: no WebAssembly screens; the vehicles show what they did before |
| `wasm.cabframes` | 4 | a screen in the cab is refreshed every so many frames (1-1000) |
| `wasm.externalframes` | 30 | a screen outside the cab (destination sign) is refreshed every so many frames |
A screen may ask for a longer interval of its own (`frames:` below); the longer of the two counts. Screens sharing
an interval are spread over its frames, so they don't all refresh on the same one. A screen in the cab is refreshed
only while the player sits in that cab, a screen outside only while its vehicle is drawn.
## .mmd
In the cab section, next to `pyscreen:`:
```
wasmscreen: { ekran.wasm target: screen01 frames: 6 parameters: id=1&mode=full }
```
| key | meaning |
|---------------|---------------------------------------------------------------------------------------------------|
| (first token) | the module, relative to the vehicle folder when it has no path of its own |
| `target:` | the submodel the screen is drawn on (its texture is replaced); `none` for a screen that only sends commands |
| `frames:` | the screen's own refresh interval, used when longer than `wasm.cabframes` |
| `parameters:` | extra `key=value` pairs, `&`-separated, added to the input |
A touch on the screen's submodel comes in the input as the list of points `touches`, as for the Python screens.
In `internaldata:`, next to `pydestinationsign:`:
```
wasmdestinationsign: { tablica.wasm frames: 60 instance: name background: tablica_off parameters: style=lcd }
```
The keys are those of `pydestinationsign:` (`instance:`, `parameters:`, `background:`) and `frames:`. The input is
what a Python sign gets: the timetable (`$timetable`), `_id1` (the train), `_id2` (the instance) and the
parameters, read anew on every refresh, so the sign can follow the journey. The vehicles of one train showing the
same request share one screen and one texture. A static destination texture, when there is one, still comes first.
A vehicle may have both `pydestinationsign:` and `wasmdestinationsign:`: with the WebAssembly screens on, the
WebAssembly one is used; with them off (or in a release without them) the Python one. An unknown key in either block
is skipped together with its value.
## The module
Exports:
| export | meaning |
|-------------------------------|------------------------------------------------------------------------------------------|
| `memory` | the linear memory |
| `screen_init() -> i32` | optional; called once after loading. anything but 0 disables the screen |
| `screen_render() -> i32` | called on every refresh. returns the address of the image, or 0 to keep the previous one |
| `screen_width() -> i32` | the width of the image, asked after every `screen_render` that returned an image |
| `screen_height() -> i32` | the height of the image, up to 4096 on either side |
The image is `width * height` pixels of 4 bytes, R G B A, in sRGB, its rows in the order the Python screens give
them (PIL's `tobytes()`).
Imports, module `eu07` (keys and texts are a pointer and a length in bytes, in the module's memory):
| import | meaning |
|-----------------------------------------------------------------|-----------------------------------------------------------------|
| `value_type(key, key_len) -> i32` | 0 no such value, 1 float, 2 int, 3 bool, 4 string, 5 list of points |
| `get_float(key, key_len) -> f64` | the value as a number (int and bool too); 0 when there is none |
| `get_int(key, key_len) -> i32` | the value as an integer (a float is truncated) |
| `get_bool(key, key_len) -> i32` | 1 or 0 |
| `get_string(key, key_len, buffer, capacity) -> i32` | copies the text, in UTF-8; returns its whole length, -1 when there is none |
| `get_vec2_count(key, key_len) -> i32` | the number of points in a list (`touches`) |
| `get_vec2(key, key_len, index, out) -> i32` | writes the point as two f32 at `out`; returns 1, or 0 when there is none |
| `command(name, name_len, param1, param2)` | sends a command (the names of `simulation::commandMap`) to the vehicle of the cab; ignored outside the cab |
| `log(text, text_len)` | writes a line to the log |
| `time() -> f64` | seconds of a steady clock shared by all screens, for animations (scrolling) that keep their pace whatever the refresh interval |
| `led_post(source, width, height, target, bloom) -> i32` | `led_util.render_post()` of the Python screens, run natively: each diode of the `width x height` image becomes 2x2 pixels with a darker grid, glowing into its neighbours when `bloom` is not 0, in a 1 pixel black frame; `target` takes `(2 width + 2) x (2 height + 2)`. returns 1, or 0 for sizes over 4096 |
| `resize_lanczos(source, width, height, target, target_width, target_height) -> i32` | Pillow's `resize(..., Image.LANCZOS)`, run natively, giving the same pixels. returns 1, or 0 for sizes over 4096 |
| `file_read(path, path_len, buffer, capacity) -> i32` | reads a file from the module's folder or below; returns the file's whole size, -1 when there is none or the path leads outside |
`led_post` and `resize_lanczos` take RGBA images in the module's memory and write alpha 255; the source and the
target may overlap. They are what makes LED boards fast enough: in the interpreter the same filters take some 30
times longer.
A call into the module may run at most 200 million instructions; a module that traps or runs over is disabled and
the reason goes to the log. The module gets 64 KiB of stack and no heap from the host: it manages its own memory.
## Building a module
With clang (any version with the `wasm32` target), no C library:
```c
// ekran.c
#include <stdint.h>
#define IMPORT(name) __attribute__((import_module("eu07"), import_name(name)))
IMPORT("get_float") double get_float(const char *key, int key_len);
IMPORT("command") void command(const char *name, int name_len, int param1, int param2);
#define WIDTH 256
#define HEIGHT 128
static uint32_t pixels[WIDTH * HEIGHT];
__attribute__((export_name("screen_width"))) int screen_width(void) { return WIDTH; }
__attribute__((export_name("screen_height"))) int screen_height(void) { return HEIGHT; }
__attribute__((export_name("screen_render"))) int screen_render(void) {
double const velocity = get_float("velocity", 8);
int const filled = (int)(velocity * WIDTH / 160.0);
for (int y = 0; y < HEIGHT; ++y)
for (int x = 0; x < WIDTH; ++x)
pixels[y * WIDTH + x] = x < filled ? 0xFF00C000u : 0xFF202020u; // A B G R, little-endian
return (int)(uintptr_t)pixels;
}
```
```
clang --target=wasm32 -O2 -nostdlib -Wl,--no-entry -o ekran.wasm ekran.c
```
Any language that compiles to WebAssembly works the same way (Rust `wasm32-unknown-unknown`, Zig, AssemblyScript),
as long as the module imports nothing but the `eu07` functions above.