From e433b43fefcc117bd27e5409b0be22f64d7c34e3 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Tue, 10 Nov 2026 16:22:35 +0100 Subject: [PATCH] Script screens: led_post and resize of the Lua screens on the gpu --- CMakeLists.txt | 1 + application/application.cpp | 6 + rendering/opengl33renderer.cpp | 15 ++ rendering/opengl33renderer.h | 3 + rendering/opengl33screenpost.cpp | 250 ++++++++++++++++++++++ rendering/opengl33screenpost.h | 77 +++++++ rendering/renderer.h | 8 + script_screens_doc.md | 15 +- scripting/luaprogram.cpp | 100 +++++++-- scripting/scriptscreenkernels.cpp | 39 ++++ scripting/scriptscreenkernels.h | 30 +++ scripting/scriptscreenprogram.h | 2 + scripting/scriptscreens.cpp | 103 ++++++++- scripting/scriptscreens.h | 24 +++ shaders/screen_led.frag | 76 +++++++ shaders/screen_resize.frag | 62 ++++++ tests/unit/test_script_screen_host.cpp | 153 +++++++++++++ tests/unit/test_script_screen_kernels.cpp | 33 +++ utilities/Globals.cpp | 7 + utilities/Globals.h | 1 + 20 files changed, 971 insertions(+), 34 deletions(-) create mode 100644 rendering/opengl33screenpost.cpp create mode 100644 rendering/opengl33screenpost.h create mode 100644 shaders/screen_led.frag create mode 100644 shaders/screen_resize.frag diff --git a/CMakeLists.txt b/CMakeLists.txt index cbcf185d..fc98ead0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -344,6 +344,7 @@ if (WITH_OPENGL_MODERN) "rendering/opengl33precipitation.cpp" "rendering/opengl33particles.cpp" "rendering/opengl33terrain.cpp" + "rendering/opengl33screenpost.cpp" "rendering/opengl33renderer.cpp") set(DEFINITIONS ${DEFINITIONS} "WITH_OPENGL_MODERN") endif() diff --git a/application/application.cpp b/application/application.cpp index 136aa060..5cf291cc 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -430,6 +430,10 @@ int eu07_application::init(int Argc, char *Argv[]) if (Global.luascreens_enabled || Global.wasm_enabled) { m_screenqueue.init(); + if (Global.screens_gpupost) + { + m_screenqueue.set_post_processor(GfxRenderer->Screen_post_processor()); + } } if ((result = init_modes()) != 0) { @@ -727,6 +731,8 @@ void eu07_application::exit() mode.reset(); step("the renderer"); + // the screens' post-processing goes with the renderer + m_screenqueue.set_post_processor(nullptr); GfxRenderer->Shutdown(); m_network.reset(); diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index d96c878f..eec136b7 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -416,6 +416,21 @@ bool opengl33_renderer::Init(GLFWwindow *Window) void opengl33_renderer::Shutdown() { vr.reset(); + m_screen_post.reset(); +} + +script_screens::post_processor *opengl33_renderer::Screen_post_processor() +{ + // GLES converts what's written into an sRGB texture, and the passes give the bytes as they are: the cpu does it there + if (Global.gfx_usegles) + { + return nullptr; + } + if (m_screen_post == nullptr) + { + m_screen_post = std::make_unique(); + } + return m_screen_post.get(); } bool opengl33_renderer::AddViewport(const global_settings::extraviewport_config &conf) diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 0e84761e..288ca0df 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -30,6 +30,7 @@ http://mozilla.org/MPL/2.0/. #include "gl/pbo.h" #include "gl/query.h" #include "rendering/opengl33terrain.h" +#include "rendering/opengl33screenpost.h" // bare-bones render controller, in lack of anything better yet class opengl33_renderer : public gfx_renderer { @@ -145,6 +146,7 @@ class opengl33_renderer : public gfx_renderer { info_stats() const override; void MakeScreenshot() override; bool Render_Preview( TAnimModel *Instance, int const Size, int const Margin, bool const Shadows, std::vector &Image ) override; + script_screens::post_processor *Screen_post_processor() override; @@ -515,6 +517,7 @@ class opengl33_renderer : public gfx_renderer { std::unique_ptr m_celestial_shader; std::unique_ptr m_hiddenarea_shader; std::unique_ptr m_copy_shader; + std::unique_ptr m_screen_post; // made on the first request std::unique_ptr m_empty_vao; diff --git a/rendering/opengl33screenpost.cpp b/rendering/opengl33screenpost.cpp new file mode 100644 index 00000000..11bdf551 --- /dev/null +++ b/rendering/opengl33screenpost.cpp @@ -0,0 +1,250 @@ +/* +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 "rendering/opengl33screenpost.h" + +#include "model/Texture.h" +#include "utilities/Logs.h" + +#include +#include + +namespace +{ + +// the state of the context the passes change, put back after them +class saved_state +{ + public: + saved_state() + { + glGetIntegerv(GL_VIEWPORT, m_viewport.data()); + glGetBooleanv(GL_COLOR_WRITEMASK, m_colormask.data()); + glGetBooleanv(GL_DEPTH_WRITEMASK, &m_depthmask); + for (std::size_t idx = 0; idx < m_capabilities.size(); ++idx) + { + m_enabled[idx] = glIsEnabled(m_capabilities[idx]); + glDisable(m_capabilities[idx]); + } + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glDepthMask(GL_FALSE); + } + ~saved_state() + { + for (std::size_t idx = 0; idx < m_capabilities.size(); ++idx) + { + if (m_enabled[idx] == GL_TRUE) + { + glEnable(m_capabilities[idx]); + } + } + glColorMask(m_colormask[0], m_colormask[1], m_colormask[2], m_colormask[3]); + glDepthMask(m_depthmask); + glViewport(m_viewport[0], m_viewport[1], m_viewport[2], m_viewport[3]); + } + saved_state(saved_state const &) = delete; + saved_state &operator=(saved_state const &) = delete; + + private: + // GL_FRAMEBUFFER_SRGB off: the passes write the bytes of the image into the screen's sRGB texture as they are, as + // update_from_memory puts them there + static constexpr std::array m_capabilities{GL_DEPTH_TEST, GL_BLEND, GL_CULL_FACE, GL_SCISSOR_TEST, GL_STENCIL_TEST, GL_FRAMEBUFFER_SRGB}; + std::array m_enabled{}; + std::array m_viewport{}; + std::array m_colormask{}; + GLboolean m_depthmask{GL_TRUE}; +}; + +void unbind_all() +{ + gl::framebuffer::unbind(); + gl::program::unbind(); + gl::vao::unbind(); +} + +// the screen's texture made the size of the result, in the format update_from_memory gives it +void prepare_target(opengl_texture &Target, int const Width, int const Height) +{ + if (Target.id != opengl_texture::invalid_id && Target.data_width == static_cast(Width) && Target.data_height == static_cast(Height) && + Target.data_format == GL_SRGB8_ALPHA8 && Target.data_components == GL_RGBA) + { + return; + } + Target.update_from_memory(Width, Height, nullptr); +} + +} // namespace + +opengl33_screen_post::~opengl33_screen_post() +{ + for (auto const *owned : {&m_source, &m_between[0], &m_between[1]}) + { + if (owned->texture != 0) + { + glDeleteTextures(1, &owned->texture); + } + } +} + +opengl33_screen_post::stage opengl33_screen_post::make_stage(gl::shader const &Vertex, std::string const &Fragment) +{ + gl::shader const fragment{Fragment}; + stage result; + result.program = std::make_unique(std::vector>{Vertex, fragment}); + auto const program{static_cast(*result.program)}; + result.source_size = glGetUniformLocation(program, "source_size"); + result.target_size = glGetUniformLocation(program, "target_size"); + result.flip = glGetUniformLocation(program, "flip"); + result.mode = glGetUniformLocation(program, "mode"); + result.program->bind(); + glUniform1i(glGetUniformLocation(program, "source"), 0); + return result; +} + +bool opengl33_screen_post::setup() +{ + if (m_failed) + { + return false; + } + if (m_led.program != nullptr) + { + return true; + } + try + { + gl::shader const vertex{"quad.vert"}; + m_led = make_stage(vertex, "screen_led.frag"); + m_resize = make_stage(vertex, "screen_resize.frag"); + } + catch (gl::shader_exception const &Error) + { + ErrorLog(std::format("Script screens: the shaders of the post-processing failed: {}", Error.what())); + m_failed = true; + return false; + } + m_vao = std::make_unique(); + m_framebuffer = std::make_unique(); + WriteLog("Script screens: the post-processing of the images runs on the gpu"); + return true; +} + +void opengl33_screen_post::store(surface &Surface, int const Width, int const Height, std::uint8_t const *Pixels) +{ + if (Surface.texture == 0) + { + glGenTextures(1, &Surface.texture); + glBindTexture(GL_TEXTURE_2D, Surface.texture); + // read with texelFetch: no mipmaps, nothing filtered + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + else + { + glBindTexture(GL_TEXTURE_2D, Surface.texture); + } + if (Surface.width != Width || Surface.height != Height) + { + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, Width, Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, Pixels); + Surface.width = Width; + Surface.height = Height; + } + else if (Pixels != nullptr) + { + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, Width, Height, GL_RGBA, GL_UNSIGNED_BYTE, Pixels); + } +} + +std::vector opengl33_screen_post::passes_of(std::span const Steps, int Width, int Height) const +{ + std::vector result; + for (auto const &step : Steps) + { + if (step.type == script_screens::post_step::kind::led) + { + Width = Width * 2 + 2; + Height = Height * 2 + 2; + result.emplace_back(&m_led, Width, Height, step.bloom ? 1 : 0); + continue; + } + result.emplace_back(&m_resize, step.width, Height, 0); + result.emplace_back(&m_resize, step.width, step.height, 1); + Width = step.width; + Height = step.height; + } + return result; +} + +bool opengl33_screen_post::draw(pass const &Pass, surface const &Source, GLuint const Target, bool const Last) const +{ + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, Target, 0); + if (auto const status{glCheckFramebufferStatus(GL_FRAMEBUFFER)}; status != GL_FRAMEBUFFER_COMPLETE) + { + ErrorLog(std::format("Script screens: the post-processing can't draw into a texture, framebuffer status {:#x}", status)); + return false; + } + glViewport(0, 0, Pass.width, Pass.height); + Pass.shader->program->bind(); + glUniform2i(Pass.shader->source_size, Source.width, Source.height); + glUniform2i(Pass.shader->target_size, Pass.width, Pass.height); + glUniform1i(Pass.shader->flip, Last ? 1 : 0); + glUniform1i(Pass.shader->mode, Pass.mode); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, Source.texture); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + return true; +} + +bool opengl33_screen_post::apply(ITexture &Target, std::span const Pixels, int const Width, int const Height, + std::span const Steps) +{ + auto *target{dynamic_cast(&Target)}; + if (target == nullptr || Steps.empty() || Pixels.size() < script_screens::image_size(Width, Height) || false == setup()) + { + return false; + } + auto const passes{passes_of(Steps, Width, Height)}; + prepare_target(*target, passes.back().width, passes.back().height); + + auto succeeded{true}; + { + saved_state const saved; + // what the binding caches of gl:: hold may not be what's bound now: bound anew, and left unbound after the passes + unbind_all(); + m_framebuffer->bind(); + m_vao->bind(); + store(m_source, Width, Height, Pixels.data()); + auto const *source{&m_source}; + for (std::size_t idx = 0; idx < passes.size() && succeeded; ++idx) + { + auto const last{idx + 1 == passes.size()}; + auto &between{m_between[idx % 2]}; + if (false == last) + { + store(between, passes[idx].width, passes[idx].height, nullptr); + } + succeeded = draw(passes[idx], *source, last ? target->id : between.texture, last); + source = ≬ + } + // the screen's texture isn't held by the framebuffer when it's made anew + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + unbind_all(); + } + if (succeeded) + { + glBindTexture(GL_TEXTURE_2D, target->id); + glGenerateMipmap(GL_TEXTURE_2D); + } + // the textures bound here went past the unit cache + opengl_texture::reset_unit_cache(); + return succeeded; +} diff --git a/rendering/opengl33screenpost.h b/rendering/opengl33screenpost.h new file mode 100644 index 00000000..bb5014e2 --- /dev/null +++ b/rendering/opengl33screenpost.h @@ -0,0 +1,77 @@ +/* +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/scriptscreens.h" +#include "gl/framebuffer.h" +#include "gl/shader.h" +#include "gl/vao.h" + +#include +#include +#include +#include +#include +#include + +// the post-processing of the script screens (image:led_post, image:resize) on the gpu, for the OpenGL 3.3 renderer: a +// pass for a led_post and two for a resize (along the rows, then along the columns), the last pass into the screen's +// texture. shaders/screen_led.frag and shaders/screen_resize.frag give the pixels of scripting/scriptscreenkernels.cpp +class opengl33_screen_post final : public script_screens::post_processor +{ + public: + opengl33_screen_post() = default; + ~opengl33_screen_post() override; + + bool apply(ITexture &Target, std::span Pixels, int Width, int Height, std::span Steps) override; + + private: + // a shader of the passes, with the places of its uniforms + struct stage + { + std::unique_ptr program; + GLint source_size{-1}; + GLint target_size{-1}; + GLint flip{-1}; + GLint mode{-1}; + }; + // a texture the passes read + struct surface + { + GLuint texture{0}; + int width{0}; + int height{0}; + }; + struct pass + { + stage const *shader; + int width; + int height; + int mode; // led: 1 with the glow; resize: 0 along the rows, 1 along the columns + }; + + // makes the shaders on the first use. false when they failed, then and from then on + bool setup(); + std::vector passes_of(std::span Steps, int Width, int Height) const; + // draws a pass from the source into the texture; Last: the rows go bottom row first, as the screen's texture takes them + bool draw(pass const &Pass, surface const &Source, GLuint Target, bool Last) const; + + static stage make_stage(gl::shader const &Vertex, std::string const &Fragment); + // sizes the texture, and fills it when there are pixels + static void store(surface &Surface, int Width, int Height, std::uint8_t const *Pixels); + + bool m_failed{false}; + stage m_led; + stage m_resize; + std::unique_ptr m_vao; + std::unique_ptr m_framebuffer; + surface m_source; // the image as drawn + std::array m_between; // what the passes before the last one give, in turn +}; diff --git a/rendering/renderer.h b/rendering/renderer.h index 1a110e68..dec3f752 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -21,6 +21,11 @@ namespace gl class program; } +namespace script_screens +{ +class post_processor; +} + namespace gfx { // material painted over the heightmap terrain, and the size (metres) its textures are repeated at; 0: the size given by the material, or 8 m @@ -130,6 +135,9 @@ public: // seen in perspective from the front, side and above and filling the image less Margin pixels at each edge. // Image receives rgb rows, top row first. returns false if the backend can't make previews or there's nothing to draw virtual auto Render_Preview( TAnimModel *Instance, int const Size, int const Margin, bool const Shadows, std::vector &Image ) -> bool { return false; } + // runs the post-processing of the script screens (image:led_post, image:resize) on the gpu. nullptr when the backend + // can't; the screens then do it on the cpu + virtual auto Screen_post_processor() -> script_screens::post_processor * { return nullptr; } }; class gfx_renderer_factory diff --git a/script_screens_doc.md b/script_screens_doc.md index d8a6811b..52ccc9ce 100644 --- a/script_screens_doc.md +++ b/script_screens_doc.md @@ -23,6 +23,7 @@ the reason goes to the log. | `wasm.enabled` | 1 | 0: no WebAssembly screens; the vehicles show what they did before | | `screens.cabframes` | 4 | a screen in the cab is refreshed every so many frames (1-1000) | | `screens.externalframes` | 30 | a screen outside the cab (destination sign) is refreshed every so many frames | +| `screens.gpupost` | 1 | 0: `led_post` and `resize` of the Lua screens always run on the cpu, as they did before | `wasm.cabframes` and `wasm.externalframes`, the first names of the last two, are read as well. @@ -118,14 +119,24 @@ glyph (`led_util` picks a random one). | `image:draw_text_right(...)`, `image:draw_text_centered(...)` | the same, ending at `x` or centred on it (`draw_strl`, `draw_strc`) | | `image:draw_sprite(sprite, x, y, color)` | the lit pixels of the sprite (`draw_spr`) | | `image:draw_image(image, x, y)` | another image, clipped | -| `image:led_post([bloom])` | `led_util.render_post()`: a new image, each pixel a 2x2 diode with a darker grid, glowing (`bloom`, on by default), in a 1 pixel black frame | -| `image:resize(width, height)` | a new image, scaled as Pillow's `resize(..., Image.LANCZOS)`, with the same pixels | +| `image:led_post([bloom])` | `led_util.render_post()`: a new image, each pixel a 2x2 diode with a darker grid, glowing (`bloom`, on by default), in a 1 pixel black frame. see below for when it runs | +| `image:resize(width, height)` | a new image, scaled as Pillow's `resize(..., Image.LANCZOS)`, with the same pixels. see below for when it runs | | `image:copy()` | a new image, the same | | `font:height()`, `font:text_width(text [, spacing])` | the height, the width of a text (`get_str_len`) | | `sprite:width()`, `sprite:height()` | the size | The drawing, `led_post` and `resize` run natively in the game, so a script only says what goes where. +`led_post` and `resize` only note what is to be done; the image's size is known right away. They run when the image is +used: + +- on the image `render` returns, on the gpu, into the screen's texture. Only the screens that ask for them pay for + them, and the texture gets the small image as drawn rather than the large one. The gpu gives the pixels of the cpu, + give or take one step of brightness here and there, from rounding in single precision. +- on the cpu, as soon as the script reads the image (`get`), draws on it, or draws it on another image. +- on the cpu, in the screens' worker, when the renderer can't do it on the gpu (the NVRHI renderer, OpenGL ES, a + shader that failed to load) or `screens.gpupost` is 0. A shader that fails is logged once, and the cpu takes over. + ## WebAssembly modules A module exports: diff --git a/scripting/luaprogram.cpp b/scripting/luaprogram.cpp index d436cd28..258929dc 100644 --- a/scripting/luaprogram.cpp +++ b/scripting/luaprogram.cpp @@ -145,9 +145,63 @@ template Type_ &check_object(lua_State *L, int const Index, cha return *object; } +// an image of a script: what was drawn on it, and the post-processing (image:led_post, image:resize) asked for and not +// run yet. the steps of the image render returns run in the queue, on the gpu when the renderer can; the steps of an +// image the script goes on to read or draw on (or with) run here, on the cpu, when it does +struct lua_image +{ + canvas drawn; + std::vector steps; + + int width() const + { + auto result{drawn.width()}; + for (auto const &step : steps) + { + result = width_after(step, result); + } + return result; + } + int height() const + { + auto result{drawn.height()}; + for (auto const &step : steps) + { + result = height_after(step, result); + } + return result; + } + // the image with its post-processing done + canvas &done() + { + if (false == steps.empty()) + { + post_image image{drawn.pixels(), drawn.width(), drawn.height()}; + // the sizes were checked when the steps were asked for + apply_post(image, steps); + drawn = canvas{image.width, image.height, std::move(image.pixels)}; + steps.clear(); + } + return drawn; + } + // the image, a step further + lua_image then(post_step const &Step) const + { + lua_image result{drawn, steps}; + result.steps.push_back(Step); + return result; + } +}; + +lua_image &check_lua_image(lua_State *L, int const Index) +{ + return check_object(L, Index, image_metatable); +} + +// the image, to read or draw on: its post-processing is done first canvas &check_image(lua_State *L, int const Index) { - return check_object(L, Index, image_metatable); + return check_lua_image(L, Index).done(); } int check_int(lua_State *L, int const Index) @@ -226,7 +280,7 @@ int api_image(lua_State *L) luaL_argcheck(L, valid_image_side(width), 1, "between 1 and 4096"); luaL_argcheck(L, valid_image_side(height), 2, "between 1 and 4096"); auto const color{lua_isnoneornil(L, 3) ? rgb{0, 0, 0} : check_color(L, 3)}; - push_object(L, canvas{width, height, color}, image_metatable); + push_object(L, lua_image{canvas{width, height, color}, {}}, image_metatable); return 1; } @@ -388,13 +442,13 @@ int api_include(lua_State *L) int image_width(lua_State *L) { - lua_pushinteger(L, check_image(L, 1).width()); + lua_pushinteger(L, check_lua_image(L, 1).width()); return 1; } int image_height(lua_State *L) { - lua_pushinteger(L, check_image(L, 1).height()); + lua_pushinteger(L, check_lua_image(L, 1).height()); return 1; } @@ -498,36 +552,39 @@ int image_draw_image(lua_State *L) return 0; } -// image:led_post([bloom]): led_util's render_post, a new image (2w + 2) x (2h + 2) +// image:led_post([bloom]): led_util's render_post, a new image (2w + 2) x (2h + 2). done when the image is shown or used int image_led_post(lua_State *L) { - auto const &image{check_image(L, 1)}; + auto const &image{check_lua_image(L, 1)}; auto const bloom{lua_isnoneornil(L, 2) || lua_toboolean(L, 2) != 0}; luaL_argcheck(L, valid_image_side(image.width() * 2 + 2) && valid_image_side(image.height() * 2 + 2), 1, "too large"); - std::vector pixels(image_size(image.width() * 2 + 2, image.height() * 2 + 2)); - led_post(image.pixels(), image.width(), image.height(), pixels, bloom); - push_object(L, canvas{image.width() * 2 + 2, image.height() * 2 + 2, std::move(pixels)}, image_metatable); + post_step step; + step.type = post_step::kind::led; + step.bloom = bloom; + push_object(L, image.then(step), image_metatable); return 1; } -// image:resize(width, height): scaled as Pillow's resize(..., LANCZOS), a new image +// image:resize(width, height): scaled as Pillow's resize(..., LANCZOS), a new image. done when the image is shown or used int image_resize(lua_State *L) { - auto const &image{check_image(L, 1)}; + auto const &image{check_lua_image(L, 1)}; auto const width{check_int(L, 2)}; auto const height{check_int(L, 3)}; luaL_argcheck(L, valid_image_side(width), 2, "between 1 and 4096"); luaL_argcheck(L, valid_image_side(height), 3, "between 1 and 4096"); - std::vector pixels(image_size(width, height)); - resize_lanczos(image.pixels(), image.width(), image.height(), pixels, width, height); - push_object(L, canvas{width, height, std::move(pixels)}, image_metatable); + post_step step; + step.type = post_step::kind::resize; + step.width = width; + step.height = height; + push_object(L, image.then(step), image_metatable); return 1; } int image_copy(lua_State *L) { - auto const &image{check_image(L, 1)}; - push_object(L, canvas{image}, image_metatable); + auto const &image{check_lua_image(L, 1)}; + push_object(L, lua_image{image}, image_metatable); return 1; } @@ -695,7 +752,7 @@ void lua_program::register_api() lua_pushlightuserdata(L, this); lua_rawset(L, LUA_REGISTRYINDEX); - make_metatable(L, image_metatable, image_methods, collect); + make_metatable(L, image_metatable, image_methods, collect); make_metatable(L, font_metatable, font_methods, collect); make_metatable(L, sprite_metatable, sprite_methods, collect); @@ -806,11 +863,12 @@ bool lua_program::render(render_call &Call) } // nil: the screen didn't change, keep the image there is auto succeeded{true}; - if (auto const *image{test_object(m_state, -1, image_metatable)}; image != nullptr) + if (auto const *image{test_object(m_state, -1, image_metatable)}; image != nullptr) { - Call.pixels = image->pixels(); - Call.width = image->width(); - Call.height = image->height(); + Call.pixels = image->drawn.pixels(); + Call.width = image->drawn.width(); + Call.height = image->drawn.height(); + Call.post = image->steps; } else if (false == lua_isnil(m_state, -1)) { diff --git a/scripting/scriptscreenkernels.cpp b/scripting/scriptscreenkernels.cpp index 2bc8e984..76284c13 100644 --- a/scripting/scriptscreenkernels.cpp +++ b/scripting/scriptscreenkernels.cpp @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include namespace script_screens @@ -239,4 +240,42 @@ bool resize_lanczos(std::span const Source, int const Width, return true; } +int width_after(post_step const &Step, int const Width) +{ + return Step.type == post_step::kind::led ? Width * 2 + 2 : Step.width; +} + +int height_after(post_step const &Step, int const Height) +{ + return Step.type == post_step::kind::led ? Height * 2 + 2 : Step.height; +} + +bool apply_post(post_image &Image, std::span const Steps) +{ + if (Steps.empty()) + { + return true; + } + auto result{Image}; + for (auto const &step : Steps) + { + auto const width{width_after(step, result.width)}; + auto const height{height_after(step, result.height)}; + if (width <= 0 || height <= 0) + { + return false; + } + std::vector pixels(image_size(width, height)); + auto const done{step.type == post_step::kind::led ? led_post(result.pixels, result.width, result.height, pixels, step.bloom) + : resize_lanczos(result.pixels, result.width, result.height, pixels, width, height)}; + if (false == done) + { + return false; + } + result = post_image{std::move(pixels), width, height}; + } + Image = std::move(result); + return true; +} + } // namespace script_screens diff --git a/scripting/scriptscreenkernels.h b/scripting/scriptscreenkernels.h index 4a9a265d..b6eee82f 100644 --- a/scripting/scriptscreenkernels.h +++ b/scripting/scriptscreenkernels.h @@ -9,8 +9,10 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include #include #include +#include // image filters the script screens call through the host (led_post, 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 @@ -33,4 +35,32 @@ bool led_post(std::span Source, int Width, int Height, std:: // for sizes that don't fit the spans bool resize_lanczos(std::span Source, int Width, int Height, std::span Target, int TargetWidth, int TargetHeight); +// one step of the post-processing a script asks for on its image (image:led_post, image:resize). the steps of the image +// render returns run on the gpu when the renderer can (script_screens::post_processor), otherwise on the cpu +struct post_step +{ + enum class kind + { + led, // led_post + resize // resize_lanczos + }; + kind type{kind::led}; + bool bloom{true}; // led: with the glow + int width{0}; // resize: the size it gives + int height{0}; +}; + +struct post_image +{ + std::vector pixels; // RGBA, from the top row down + int width{0}; + int height{0}; +}; + +// the size of an image Width wide, or Height high, after the step +int width_after(post_step const &Step, int Width); +int height_after(post_step const &Step, int Height); +// runs the steps on the cpu, one after another. false for sizes the kernels refuse; the image is then as it was +bool apply_post(post_image &Image, std::span Steps); + } // namespace script_screens diff --git a/scripting/scriptscreenprogram.h b/scripting/scriptscreenprogram.h index af117849..6b0d1b70 100644 --- a/scripting/scriptscreenprogram.h +++ b/scripting/scriptscreenprogram.h @@ -47,6 +47,8 @@ struct render_call std::vector pixels; int width{0}; int height{0}; + // the post-processing still to run on the image (image:led_post, image:resize of the Lua screens) + std::vector post; }; // a running script: a Lua state, a WebAssembly instance. made, called and destroyed on the worker thread, or by the diff --git a/scripting/scriptscreens.cpp b/scripting/scriptscreens.cpp index 53dc7285..08314932 100644 --- a/scripting/scriptscreens.cpp +++ b/scripting/scriptscreens.cpp @@ -31,6 +31,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include #include @@ -129,6 +130,32 @@ std::string_view language_name(script_language const Language) return Language == script_language::lua ? "Lua" : "WASM"; } +// the rows of the image the other way round. the script draws from the top row down; the texture takes the bottom row +// first, as the python screens give it (abstractscreenrenderer flips the image before handing it over) +std::vector flipped(std::span const Pixels, int const Width, int const Height) +{ + auto const row{static_cast(Width) * 4}; + auto const height{static_cast(Height)}; + std::vector result(Pixels.size()); + for (std::size_t y = 0; y < height; ++y) + { + std::memcpy(result.data() + (height - 1 - y) * row, Pixels.data() + y * row, row); + } + return result; +} + +// the post-processing of the image done on the cpu. false when the kernels refused a size +bool post_on_cpu(std::vector &Pixels, int &Width, int &Height, std::vector &Steps) +{ + script_screens::post_image image{std::move(Pixels), Width, Height}; + auto const done{script_screens::apply_post(image, Steps)}; + Pixels = std::move(image.pixels); + Width = image.width; + Height = image.height; + Steps.clear(); + return done; +} + } // namespace class script_screen @@ -159,11 +186,13 @@ class script_screen // the worker's, and the queue's once the worker stopped std::unique_ptr program; - // the image, handed from the worker to the main thread, bottom row first as the texture takes it + // the image, handed from the worker to the main thread: bottom row first as the texture takes it, or, while it has + // post-processing to run on the gpu, from the top row down as the script drew it std::mutex image_mutex; std::vector image; int image_width{0}; int image_height{0}; + std::vector post; 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 @@ -188,7 +217,9 @@ struct script_screen_queue::implementation return Language == script_language::lua ? lua.get() : wasm.get(); } static void present(script_screen &Screen, script_screens::render_call &Call); - static void upload(script_screen &Screen); + void upload(script_screen &Screen); + // the post-processing of the image the gpu couldn't do, done on the cpu. false when it failed too + static bool finish_on_cpu(script_screen &Screen); static void execute(script_screens::pending_command const &Command); std::unique_ptr lua; @@ -196,6 +227,11 @@ struct script_screen_queue::implementation bool initialized{false}; std::atomic frame{0}; + // the main thread's: the gpu post-processing, none when the renderer has none, or it failed + script_screens::post_processor *post_processor{nullptr}; + // read by the worker: the post-processing is left to the main thread, for the gpu + std::atomic post_on_gpu{false}; + std::mutex screens_mutex; std::unordered_map> screens; @@ -273,6 +309,12 @@ void script_screen_queue::implementation::process(task const &Task) screen.failed = true; return; } + if (false == call.post.empty() && false == post_on_gpu && false == post_on_cpu(call.pixels, call.width, call.height, call.post)) + { + ErrorLog(std::format("{} screen \"{}\": the post-processing of the image failed", language_name(screen.language), screen.script)); + screen.failed = true; + return; + } present(screen, call); } if (false == call.commands.empty()) @@ -282,23 +324,37 @@ void script_screen_queue::implementation::process(task const &Task) } } -// keeps the image the screen drew. the script draws from the top row down; the texture takes the bottom row first, as -// the python screens give it (abstractscreenrenderer flips the image before handing it over) +// keeps the image the screen drew, turned bottom row first for the texture; with post-processing left for the gpu, as +// it was drawn void script_screen_queue::implementation::present(script_screen &Screen, script_screens::render_call &Call) { - auto const row{static_cast(Call.width) * 4}; - auto const height{static_cast(Call.height)}; std::scoped_lock lock{Screen.image_mutex}; - Screen.image.resize(Call.pixels.size()); - for (std::size_t y = 0; y < height; ++y) + if (Call.post.empty()) { - std::memcpy(Screen.image.data() + (height - 1 - y) * row, Call.pixels.data() + y * row, row); + Screen.image = flipped(Call.pixels, Call.width, Call.height); } + else + { + Screen.image = std::move(Call.pixels); + } + Screen.post = std::move(Call.post); Screen.image_width = Call.width; Screen.image_height = Call.height; Screen.image_pending = true; } +bool script_screen_queue::implementation::finish_on_cpu(script_screen &Screen) +{ + if (false == post_on_cpu(Screen.image, Screen.image_width, Screen.image_height, Screen.post)) + { + ErrorLog(std::format("{} screen \"{}\": the post-processing of the image failed", language_name(Screen.language), Screen.script)); + Screen.image.clear(); + return false; + } + Screen.image = flipped(Screen.image, Screen.image_width, Screen.image_height); + return true; +} + void script_screen_queue::implementation::upload(script_screen &Screen) { if (Screen.texture == nullptr) @@ -315,7 +371,26 @@ void script_screen_queue::implementation::upload(script_screen &Screen) { return; } - Screen.texture->update_from_memory(Screen.image_width, Screen.image_height, Screen.image.data()); + auto const on_gpu{false == Screen.post.empty() && post_processor != nullptr && + post_processor->apply(*Screen.texture, Screen.image, Screen.image_width, Screen.image_height, Screen.post)}; + if (false == on_gpu) + { + if (false == Screen.post.empty()) + { + // the gpu can't do it: the cpu does, here this time, and on the worker from now on + if (post_processor != nullptr) + { + WriteLog("Script screens: the post-processing of the images goes to the cpu"); + } + post_processor = nullptr; + post_on_gpu = false; + if (false == finish_on_cpu(Screen)) + { + 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; } @@ -481,7 +556,7 @@ void script_screen_queue::update() { if (auto const live{screen.lock()}; live != nullptr) { - implementation::upload(*live); + m_impl->upload(*live); } } } @@ -496,6 +571,12 @@ void script_screen_queue::update() } } +void script_screen_queue::set_post_processor(script_screens::post_processor *Processor) +{ + m_impl->post_processor = Processor; + m_impl->post_on_gpu = Processor != nullptr; +} + #ifndef WITH_LUA std::unique_ptr script_screens::make_lua_runtime() { diff --git a/scripting/scriptscreens.h b/scripting/scriptscreens.h index fb228581..507da954 100644 --- a/scripting/scriptscreens.h +++ b/scripting/scriptscreens.h @@ -10,16 +10,37 @@ http://mozilla.org/MPL/2.0/. #pragma once #include "scripting/scriptscreenrules.h" +#include "scripting/scriptscreenkernels.h" #include #include #include +#include #include #include struct ITexture; struct dictionary_source; +namespace script_screens +{ + +// runs the post-processing of a screen's image (image:led_post, image:resize) on the gpu, into the screen's texture. +// given by the renderer that can (gfx_renderer::Screen_post_processor); used on the main thread +class post_processor +{ + public: + post_processor() = default; + virtual ~post_processor() = default; + post_processor(post_processor const &) = delete; + post_processor &operator=(post_processor const &) = delete; + // Pixels: the image drawn, Width x Height, RGBA from the top row down. the texture gets the result bottom row first, + // as update_from_memory takes it, with its mipmaps. false when it can't; the screen then does the steps on the cpu + virtual bool apply(ITexture &Target, std::span Pixels, int Width, int Height, std::span Steps) = 0; +}; + +} // namespace script_screens + // screens drawn by scripts, the counterpart of the python screens (scripting/PyInt.h): Lua scripts run by LuaJIT // (luascreen:, luadestinationsign:) and WebAssembly modules run by WAMR (wasmscreen:, wasmdestinationsign:). a script // gets the state a python screen gets, returns an RGBA image and, in the cab, may send commands. the interface is @@ -91,6 +112,9 @@ class script_screen_queue // main thread, once per rendered frame: uploads the finished images, runs the commands the screens sent and // counts the frame void update(); + // main thread: the post-processing of the images goes to the gpu through the processor; with none (the default) it + // runs on the worker, on the cpu + void set_post_processor(script_screens::post_processor *Processor); private: struct implementation; diff --git a/shaders/screen_led.frag b/shaders/screen_led.frag new file mode 100644 index 00000000..4ee15919 --- /dev/null +++ b/shaders/screen_led.frag @@ -0,0 +1,76 @@ +// image:led_post of the script screens on the gpu (rendering/opengl33screenpost.cpp), the pixels of led_post in +// scripting/scriptscreenkernels.cpp (led_util.render_post): each diode becomes 2x2 pixels with a darker grid between +// them, glowing into its neighbours, in a 1 pixel black frame + +uniform sampler2D source; // the image, from the top row down +uniform ivec2 source_size; +uniform ivec2 target_size; +uniform int flip; // 1: the rows are written bottom row first, as the screen's texture takes them +uniform int mode; // 1: with the glow + +layout(location = 0) out vec4 out_color; + +const int glow_radius = 6; +const float glow_factor = 0.08; + +ivec3 diode(ivec2 at) +{ + return ivec3(round(texelFetch(source, at, 0).rgb * 255.0)); +} + +// the light gathered from the diodes around, weighted 1 / (0.08 + distance squared) +ivec3 glow(ivec2 at) +{ + vec3 sum = vec3(0.0); + int left = max(at.x - glow_radius, 0); + int right = min(at.x + glow_radius + 1, source_size.x); + int top = max(at.y - glow_radius, 0); + int bottom = min(at.y + glow_radius + 1, source_size.y); + for (int i = left; i < right; ++i) + { + float m = glow_factor + float((i - at.x) * (i - at.x)); + if (m > 20.0) + continue; + for (int j = top; j < bottom; ++j) + { + float s = m + float((j - at.y) * (j - at.y)); + if (s > 20.0) + continue; + sum += vec3(diode(ivec2(i, j))) / s; + } + } + return ivec3(sum * glow_factor); +} + +// the light through the grid, rounded down as led_util gets it in double precision. there 1 - 0.15 gives the exact +// result, but 1 - 2 * 0.15 is a little below 0.7, which takes these lights one lower (above 360 the result is clamped +// to 255 anyway) +int shaded(int light, int grid) +{ + if (grid == 0) + return light; + if (grid == 1) + return light * 85 / 100; + int result = light * 70 / 100; + if (light == 90 || light == 170 || light == 180 || (light >= 330 && light <= 360 && light % 10 == 0)) + result -= 1; + return result; +} + +void main() +{ + ivec2 at = ivec2(gl_FragCoord.xy); + if (flip != 0) + at.y = target_size.y - 1 - at.y; + if (at.x == 0 || at.y == 0 || at.x == target_size.x - 1 || at.y == target_size.y - 1) + { + out_color = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + ivec2 pixel = at - 1; + ivec2 lit = pixel / 2; + ivec3 light = (mode != 0) ? glow(lit) : diode(lit); + int grid = pixel.x % 2 + pixel.y % 2; + ivec3 result = ivec3(shaded(light.r, grid), shaded(light.g, grid), shaded(light.b, grid)); + out_color = vec4(vec3(clamp(result, 0, 255)) / 255.0, 1.0); +} diff --git a/shaders/screen_resize.frag b/shaders/screen_resize.frag new file mode 100644 index 00000000..3def683a --- /dev/null +++ b/shaders/screen_resize.frag @@ -0,0 +1,62 @@ +// image:resize of the script screens on the gpu (rendering/opengl33screenpost.cpp): one pass of Pillow's resize with +// the Lanczos filter, along the rows (mode 0) or along the columns (mode 1), rounded to 8 bits, as resize_lanczos in +// scripting/scriptscreenkernels.cpp does it + +uniform sampler2D source; // the image, from the top row down +uniform ivec2 source_size; +uniform ivec2 target_size; +uniform int flip; // 1: the rows are written bottom row first, as the screen's texture takes them +uniform int mode; + +layout(location = 0) out vec4 out_color; + +const float pi = 3.14159265358979; + +float sinc(float x) +{ + if (x == 0.0) + return 1.0; + x *= pi; + return sin(x) / x; +} + +float lanczos(float x) +{ + if (x >= -3.0 && x < 3.0) + return sinc(x) * sinc(x / 3.0); + return 0.0; +} + +void main() +{ + ivec2 at = ivec2(gl_FragCoord.xy); + if (flip != 0) + at.y = target_size.y - 1 - at.y; + bool vertical = mode != 0; + int index = vertical ? at.y : at.x; + int from_size = vertical ? source_size.y : source_size.x; + int to_size = vertical ? target_size.y : target_size.x; + + float scale = float(from_size) / float(to_size); + float filterscale = max(scale, 1.0); + float support = 3.0 * filterscale; + float center = (float(index) + 0.5) * scale; + float ss = 1.0 / filterscale; + int first = max(int(center - support + 0.5), 0); + int count = min(int(center + support + 0.5), from_size) - first; + + float total = 0.0; + for (int x = 0; x < count; ++x) + total += lanczos((float(x + first) - center + 0.5) * ss); + + vec3 sum = vec3(0.0); + for (int x = 0; x < count; ++x) + { + float weight = lanczos((float(x + first) - center + 0.5) * ss); + if (total != 0.0) + weight /= total; + ivec2 tap = vertical ? ivec2(at.x, first + x) : ivec2(first + x, at.y); + sum += round(texelFetch(source, tap, 0).rgb * 255.0) * weight; + } + out_color = vec4(clamp(floor(sum + 0.5), 0.0, 255.0) / 255.0, 1.0); +} diff --git a/tests/unit/test_script_screen_host.cpp b/tests/unit/test_script_screen_host.cpp index 63ffea5d..705082db 100644 --- a/tests/unit/test_script_screen_host.cpp +++ b/tests/unit/test_script_screen_host.cpp @@ -593,4 +593,157 @@ TEST_CASE("lua screens: a script includes another and reads a font of the python queue.exit(); } +namespace { + +// 2x1, red and green, shown through led_post without the glow and a resize to 4x3 +constexpr std::string_view posted_script{R"( +return { + render = function(state) + local image = eu07.image(2, 1, { 255, 0, 0 }) + image:set(1, 0, { 0, 255, 0 }) + return image:led_post(false):resize(4, 3) + end +} +)"}; + +// reads an image after led_post: its size, a pixel of the frame and one of a diode +constexpr std::string_view read_posted_script{R"( +return { + render = function(state) + local posted = eu07.image(2, 1, { 255, 255, 255 }):led_post(false) + local width, height = posted:width(), posted:height() + local frame = posted:get(0, 0) + local diode = posted:get(1, 1) + return eu07.image(1, 1, { width * 10 + height, frame, diode }) + end +} +)"}; + +// a gpu that keeps what it was given, or one that can't do it +struct recording_processor : script_screens::post_processor { + bool apply(ITexture & /*Target*/, std::span const Pixels, int const Width, int const Height, + std::span const Steps) override { + ++calls; + pixels.assign(Pixels.begin(), Pixels.end()); + width = Width; + height = Height; + steps.assign(Steps.begin(), Steps.end()); + return works; + } + + bool works{true}; + int calls{0}; + std::vector pixels; + int width{0}; + int height{0}; + std::vector steps; +}; + +// what posted_script shows, done on the cpu, bottom row first +std::vector posted_on_cpu() { + script_screens::post_step led; + led.type = script_screens::post_step::kind::led; + led.bloom = false; + script_screens::post_step resize; + resize.type = script_screens::post_step::kind::resize; + resize.width = 4; + resize.height = 3; + std::array const steps{led, resize}; + script_screens::post_image image{{255, 0, 0, 255, 0, 255, 0, 255}, 2, 1}; + REQUIRE(script_screens::apply_post(image, steps)); + std::vector result; + for (auto row = image.height - 1; row >= 0; --row) { + auto const first{image.pixels.begin() + static_cast(script_screens::image_size(image.width, row))}; + result.insert(result.end(), first, first + image.width * 4); + } + return result; +} + +} // namespace + +TEST_CASE("lua screens: led_post and resize of the image shown go to the gpu, with the image as drawn") { + script_folder const folder{"lua_post_gpu"}; + auto const script{folder.write("posted.lua", posted_script)}; + script_screen_queue queue; + REQUIRE(queue.init()); + recording_processor gpu; + queue.set_post_processor(&gpu); + + recording_texture texture; + auto const screen{queue.acquire(request(script_language::lua, script, "posted", &texture, script_screen_kind::external))}; + refresh(queue, screen, speed(0.0), 1); + REQUIRE(run_until(queue, [&gpu] { return gpu.calls > 0; })); + CHECK(texture.uploads == 0); + CHECK(gpu.width == 2); + CHECK(gpu.height == 1); + CHECK(gpu.pixels == std::vector{255, 0, 0, 255, 0, 255, 0, 255}); + REQUIRE(gpu.steps.size() == 2); + CHECK(gpu.steps[0].type == script_screens::post_step::kind::led); + CHECK_FALSE(gpu.steps[0].bloom); + CHECK(gpu.steps[1].type == script_screens::post_step::kind::resize); + CHECK(gpu.steps[1].width == 4); + CHECK(gpu.steps[1].height == 3); + queue.exit(); +} + +TEST_CASE("lua screens: without a gpu, led_post and resize run on the cpu") { + script_folder const folder{"lua_post_cpu"}; + auto const script{folder.write("posted.lua", posted_script)}; + script_screen_queue queue; + REQUIRE(queue.init()); + + recording_texture texture; + auto const screen{queue.acquire(request(script_language::lua, script, "posted", &texture, script_screen_kind::external))}; + refresh(queue, screen, speed(0.0), 1); + REQUIRE(run_until(queue, [&texture] { return texture.uploads > 0; })); + CHECK(texture.width == 4); + CHECK(texture.height == 3); + CHECK(texture.pixels == posted_on_cpu()); + queue.exit(); +} + +TEST_CASE("lua screens: when the gpu can't do the post-processing, the cpu does it from then on") { + script_folder const folder{"lua_post_fallback"}; + auto const script{folder.write("posted.lua", posted_script)}; + script_screen_queue queue; + REQUIRE(queue.init()); + recording_processor gpu; + gpu.works = false; + queue.set_post_processor(&gpu); + + recording_texture texture; + auto const screen{queue.acquire(request(script_language::lua, script, "posted", &texture, script_screen_kind::external))}; + refresh(queue, screen, speed(0.0), 1); + REQUIRE(run_until(queue, [&texture] { return texture.uploads > 0; })); + CHECK(gpu.calls == 1); + CHECK(texture.width == 4); + CHECK(texture.pixels == posted_on_cpu()); + + refresh(queue, screen, speed(0.0), 1); + REQUIRE(run_until(queue, [&texture] { return texture.uploads > 1; })); + CHECK(gpu.calls == 1); + CHECK(texture.pixels == posted_on_cpu()); + queue.exit(); +} + +TEST_CASE("lua screens: an image read after led_post has its post-processing done first") { + script_folder const folder{"lua_post_read"}; + auto const script{folder.write("read.lua", read_posted_script)}; + script_screen_queue queue; + REQUIRE(queue.init()); + recording_processor gpu; + queue.set_post_processor(&gpu); + + recording_texture texture; + auto const screen{queue.acquire(request(script_language::lua, script, "read", &texture, script_screen_kind::external))}; + refresh(queue, screen, speed(0.0), 1); + REQUIRE(run_until(queue, [&texture] { return texture.uploads > 0; })); + CHECK(gpu.calls == 0); + REQUIRE(texture.pixels.size() == 4); + CHECK(texture.pixels[0] == 64); // 6 x 4 + CHECK(texture.pixels[1] == 0); + CHECK(texture.pixels[2] == 255); + queue.exit(); +} + #endif diff --git a/tests/unit/test_script_screen_kernels.cpp b/tests/unit/test_script_screen_kernels.cpp index 445cc8a7..09b10c28 100644 --- a/tests/unit/test_script_screen_kernels.cpp +++ b/tests/unit/test_script_screen_kernels.cpp @@ -119,3 +119,36 @@ TEST_CASE("script screen kernels: images that don't fit their buffers are refuse CHECK_FALSE(script_screens::resize_lanczos(input, 5, 3, output, 12, 8)); CHECK_FALSE(script_screens::resize_lanczos(input, 6, 3, output, 2, 2)); } + +TEST_CASE("script screen kernels: the post-processing steps of an image run one after another") { + script_screens::post_step led; + led.type = script_screens::post_step::kind::led; + led.bloom = true; + script_screens::post_step resize; + resize.type = script_screens::post_step::kind::resize; + resize.width = 17; + resize.height = 11; + std::array const steps{led, resize}; + + CHECK(script_screens::width_after(led, 5) == 12); + CHECK(script_screens::height_after(led, 3) == 8); + CHECK(script_screens::width_after(resize, 12) == 17); + CHECK(script_screens::height_after(resize, 8) == 11); + + script_screens::post_image image{rgba(source), 5, 3}; + REQUIRE(script_screens::apply_post(image, steps)); + CHECK(image.width == 17); + CHECK(image.height == 11); + CHECK(image.pixels == rgba(enlarged)); + + // nothing to do, nothing done + script_screens::post_image unchanged{rgba(source), 5, 3}; + REQUIRE(script_screens::apply_post(unchanged, {})); + CHECK(unchanged.pixels == rgba(source)); + + // a size the kernels refuse leaves the image as it was + script_screens::post_image tooshort{std::vector(10), 5, 3}; + CHECK_FALSE(script_screens::apply_post(tooshort, steps)); + CHECK(tooshort.width == 5); + CHECK(tooshort.pixels.size() == 10); +} diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 0e622649..7e218803 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -912,6 +912,12 @@ bool global_settings::ConfigParseScriptScreens(cParser& Parser, std::string_view return true; } + if (token == "screens.gpupost") + { + ParseOne(Parser, screens_gpupost, 1); + return true; + } + return false; } @@ -1885,6 +1891,7 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "wasm.enabled", wasm_enabled ); export_as_text( Output, "screens.cabframes", screens_cabframes ); export_as_text( Output, "screens.externalframes", screens_externalframes ); + export_as_text( Output, "screens.gpupost", screens_gpupost ); export_as_text( Output, "async.trainThreads", trainThreads ); for( auto const &server : network_servers ) { Output diff --git a/utilities/Globals.h b/utilities/Globals.h index 75df88bc..b006fe9c 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -300,6 +300,7 @@ struct global_settings { bool wasm_enabled{true}; // screens drawn by WebAssembly modules (wasmscreen:, wasmdestinationsign: in the .mmd) int screens_cabframes{4}; // the script screens in the cab are refreshed every so many rendered frames int screens_externalframes{30}; // the script screens outside the cab (destination signs) are refreshed every so many rendered frames + bool screens_gpupost{true}; // the post-processing of the script screens (image:led_post, image:resize) on the gpu, when the renderer can bool gfx_skiprendering = false; int gfx_framebuffer_width = -1;