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:
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
67
CMake_modules/wamr/CMakeLists.txt
Normal file
67
CMake_modules/wamr/CMakeLists.txt
Normal 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")
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
1
ref/wamr
Submodule
Submodule ref/wamr added at 25bd7eb63e
242
scripting/wasmscreenkernels.cpp
Normal file
242
scripting/wasmscreenkernels.cpp
Normal 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
|
||||
36
scripting/wasmscreenkernels.h
Normal file
36
scripting/wasmscreenkernels.h
Normal 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
|
||||
126
scripting/wasmscreenrules.cpp
Normal file
126
scripting/wasmscreenrules.cpp
Normal 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
|
||||
74
scripting/wasmscreenrules.h
Normal file
74
scripting/wasmscreenrules.h
Normal 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
942
scripting/wasmscreens.cpp
Normal 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
72
scripting/wasmscreens.h
Normal 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;
|
||||
};
|
||||
51
scripting/wasmscreensstub.cpp
Normal file
51
scripting/wasmscreensstub.cpp
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
288
tests/unit/test_wasm_host.cpp
Normal file
288
tests/unit/test_wasm_host.cpp
Normal 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
|
||||
121
tests/unit/test_wasm_kernels.cpp
Normal file
121
tests/unit/test_wasm_kernels.cpp
Normal 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));
|
||||
}
|
||||
103
tests/unit/test_wasm_screens.cpp
Normal file
103
tests/unit/test_wasm_screens.cpp
Normal 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"));
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
129
wasm_screens_doc.md
Normal 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.
|
||||
Reference in New Issue
Block a user