From b6fcba2ba3905bbfeb1eab003a0ef1996c3525d9 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 7 Apr 2024 19:30:47 -0500 Subject: [PATCH] prepare for software renderer do not use or your Furnace will need a replacement --- CMakeLists.txt | 3 + extern/imgui_software_renderer/.gitignore | 2 + extern/imgui_software_renderer/README.md | 44 ++ extern/imgui_software_renderer/imgui_sw.cpp | 703 ++++++++++++++++++++ extern/imgui_software_renderer/imgui_sw.hpp | 58 ++ src/gui/gui.h | 6 +- src/gui/render.cpp | 7 + src/gui/render/renderSoftware.cpp | 144 ++++ src/gui/render/renderSoftware.h | 51 ++ src/gui/settings.cpp | 4 + 10 files changed, 1020 insertions(+), 2 deletions(-) create mode 100644 extern/imgui_software_renderer/.gitignore create mode 100644 extern/imgui_software_renderer/README.md create mode 100644 extern/imgui_software_renderer/imgui_sw.cpp create mode 100644 extern/imgui_software_renderer/imgui_sw.hpp create mode 100644 src/gui/render/renderSoftware.cpp create mode 100644 src/gui/render/renderSoftware.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c785003d3..1686bc77c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -769,6 +769,7 @@ extern/imgui_patched/imgui_draw.cpp extern/imgui_patched/imgui_tables.cpp extern/imgui_patched/imgui_widgets.cpp extern/imgui_patched/backends/imgui_impl_sdl2.cpp +extern/imgui_software_renderer/imgui_sw.cpp extern/imgui_patched/misc/cpp/imgui_stdlib.cpp extern/igfd/ImGuiFileDialog.cpp @@ -776,6 +777,7 @@ src/gui/plot_nolerp.cpp src/gui/render.cpp src/gui/render/abstract.cpp +src/gui/render/renderSoftware.cpp src/gui/font_exo.cpp src/gui/font_liberationSans.cpp @@ -1010,6 +1012,7 @@ if (BUILD_GUI) list(APPEND DEPENDENCIES_INCLUDE_DIRS extern/imgui_patched extern/imgui_patched/backends + extern/imgui_software_renderer extern/igfd ) if (WIN32 OR APPLE) diff --git a/extern/imgui_software_renderer/.gitignore b/extern/imgui_software_renderer/.gitignore new file mode 100644 index 000000000..e1508d3c7 --- /dev/null +++ b/extern/imgui_software_renderer/.gitignore @@ -0,0 +1,2 @@ +/build/* +*.bin diff --git a/extern/imgui_software_renderer/README.md b/extern/imgui_software_renderer/README.md new file mode 100644 index 000000000..ce20ba7ca --- /dev/null +++ b/extern/imgui_software_renderer/README.md @@ -0,0 +1,44 @@ +# MODIFIED + +this is a modified version of `https://github.com/JesusKrists/imgui_software_renderer`, which itself is a modified version of `https://github.com/emilk/imgui_software_renderer`. + +# Dear ImGui software renderer +This is a software renderer for [Dear ImGui](https://github.com/ocornut/imgui). +I built it not out of a specific need, but because it was fun. +The goal was to get something accurate and decently fast in not too many lines of code. +It renders a complex GUI in 1-10 milliseconds on a modern laptop. + +## What it is: +As the name implies, this is a software renderer for ImGui. It does not handle any windows or input. In the supplied example I use [SDL2](www.libsdl.org) for that. + +## How to use it +Just copy `imgui_sw.hpp` and `imgui_sw.cpp`. There are no other dependencies beside Dear ImGui. Requires C++11. + +## How to test it +``` +git clone https://github.com/emilk/imgui_software_renderer.git +cd imgui_software_renderer +git submodule update --init --recursive +./build_and_run.sh +``` + +For the example to work you will need to have SDL2 on your system. + +## Example: +This renders in 7 ms on my MacBook Pro: + +![Software rendered](screenshots/imgui_sw.png) + +## Alternatives +There is another software rasterizer for ImGui (which I did not know about when I wrote mine) at https://github.com/sronsse/imgui/tree/sw_rasterizer_example/examples/sdl_sw_example. +I have not compared the two (yet). + +## Future work: +* We do not yet support painting with any other texture than the default font texture. +* Optimize rendering of gradient rectangles (common for color pickers) +* Compare my software renderer to [the one by](https://github.com/sronsse/imgui/tree/sw_rasterizer_example/examples/sdl_sw_example) @sronsse + +## License: +This software is dual-licensed to the public domain and under the following +license: you are granted a perpetual, irrevocable license to copy, modify, +publish, and distribute this file as you see fit. diff --git a/extern/imgui_software_renderer/imgui_sw.cpp b/extern/imgui_software_renderer/imgui_sw.cpp new file mode 100644 index 000000000..b13a94c21 --- /dev/null +++ b/extern/imgui_software_renderer/imgui_sw.cpp @@ -0,0 +1,703 @@ +// By Emil Ernerfeldt 2018 +// LICENSE: +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. + +#include "imgui.h" +#ifndef IMGUI_DISABLE +#include "imgui_sw.hpp" + +#include +#include +#include +#include + +struct ImGui_ImplSW_Data +{ + SDL_Window* Window; + SWTexture* FontTexture; + + ImGui_ImplSW_Data() { memset((void*)this, 0, sizeof(*this)); } +}; + +struct SwOptions +{ + bool optimize_text = true;// No reason to turn this off. + bool optimize_rectangles = true;// No reason to turn this off. +}; + +static ImGui_ImplSW_Data* ImGui_ImplSW_GetBackendData() +{ + return ImGui::GetCurrentContext() ? (ImGui_ImplSW_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; +} + +// TODO: de-namespace and static-ize +namespace { + struct PaintTarget + { + uint32_t *pixels; + int width; + int height; + ImVec2 scale;// Multiply ImGui (point) coordinates with this to get pixel coordinates. + ImVec2 DisplayPos; + }; + + // ---------------------------------------------------------------------------- + +#pragma pack(push, 1) + struct ColorInt + { + uint8_t r, g, b, a = 0; + ColorInt(const std::string &) {} + + + ColorInt &operator*=(const ColorInt &other) + { + r = r * other.r / 255; + g = g * other.g / 255; + b = b * other.b / 255; + a = a * other.a / 255; + return *this; + } + }; +#pragma pack(pop) + + uint32_t blend(const ColorInt &target, const ColorInt &source) + { + if (source.a >= 255) return *reinterpret_cast(&source); + return (target.a << 24u) | (((source.b * source.a + target.b * (255 - source.a)) / 255) << 16u) + | (((source.g * source.a + target.g * (255 - source.a)) / 255) << 8u) + | ((source.r * source.a + target.r * (255 - source.a)) / 255); + } + + // ---------------------------------------------------------------------------- + // Used for interpolating vertex attributes (color and texture coordinates) in a triangle. + + struct Barycentric + { + float w0, w1, w2; + }; + + Barycentric operator*(const float f, const Barycentric &va) { return { f * va.w0, f * va.w1, f * va.w2 }; } + + void operator+=(Barycentric &a, const Barycentric &b) + { + a.w0 += b.w0; + a.w1 += b.w1; + a.w2 += b.w2; + } + + Barycentric operator+(const Barycentric &a, const Barycentric &b) + { + return Barycentric{ a.w0 + b.w0, a.w1 + b.w1, a.w2 + b.w2 }; + } + + // ---------------------------------------------------------------------------- + // Useful operators on ImGui vectors: + + ImVec2 operator*(const float f, const ImVec2 &v) { return ImVec2{ f * v.x, f * v.y }; } + + bool operator!=(const ImVec2 &a, const ImVec2 &b) { return a.x != b.x || a.y != b.y; } + + ImVec4 operator*(const float f, const ImVec4 &v) { return ImVec4{ f * v.x, f * v.y, f * v.z, f * v.w }; } + + // ---------------------------------------------------------------------------- + // Copies of functions in ImGui, inlined for speed: + + ImVec4 color_convert_u32_to_float4(ImU32 in) + { + const float s = 1.0f / 255.0f; + return ImVec4(((in >> IM_COL32_R_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_G_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_B_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_A_SHIFT) & 0xFF) * s); + } + + ImU32 color_convert_float4_to_u32(const ImVec4 &in) + { + ImU32 out; + out = uint32_t(in.x * 255.0f + 0.5f) << IM_COL32_R_SHIFT; + out |= uint32_t(in.y * 255.0f + 0.5f) << IM_COL32_G_SHIFT; + out |= uint32_t(in.z * 255.0f + 0.5f) << IM_COL32_B_SHIFT; + out |= uint32_t(in.w * 255.0f + 0.5f) << IM_COL32_A_SHIFT; + return out; + } + + // ---------------------------------------------------------------------------- + // For fast and subpixel-perfect triangle rendering we used fixed point arithmetic. + // To keep the code simple we use 64 bits to avoid overflows. + // TODO: make it 32-bit or else + + using Int = int64_t; + const Int kFixedBias = 256; + + struct Point + { + Int x, y; + }; + + Int orient2d(const Point &a, const Point &b, const Point &c) + { + return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); + } + + Int as_int(float v) { return static_cast(floor(v * kFixedBias)); } + + Point as_point(ImVec2 v) { return Point{ as_int(v.x), as_int(v.y) }; } + + // ---------------------------------------------------------------------------- + + float min3(float a, float b, float c) + { + if (a < b && a < c) { return a; } + return b < c ? b : c; + } + + float max3(float a, float b, float c) + { + if (a > b && a > c) { return a; } + return b > c ? b : c; + } + + float barycentric(const ImVec2 &a, const ImVec2 &b, const ImVec2 &point) + { + return (b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x); + } + + inline uint8_t sample_font_texture(const SWTexture &texture, int x, int y) + { + return reinterpret_cast(texture.pixels)[x + y * texture.width]; + } + + inline uint32_t sample_texture(const SWTexture &texture, int x, int y) { return texture.pixels[x + y * texture.width]; } + + void paint_uniform_rectangle(const PaintTarget &target, + const ImVec2 &min_f, + const ImVec2 &max_f, + const ColorInt &color) + { + // Integer bounding box [min, max): + int min_x_i = static_cast(target.scale.x * min_f.x + 0.5f); + int min_y_i = static_cast(target.scale.y * min_f.y + 0.5f); + int max_x_i = static_cast(target.scale.x * max_f.x + 0.5f); + int max_y_i = static_cast(target.scale.y * max_f.y + 0.5f); + + // Clamp to render target: + min_x_i = std::max(min_x_i, 0); + min_y_i = std::max(min_y_i, 0); + max_x_i = std::min(max_x_i, target.width); + max_y_i = std::min(max_y_i, target.height); + + // We often blend the same colors over and over again, so optimize for this (saves 25% total cpu): + uint32_t last_target_pixel = target.pixels[min_y_i * target.width + min_x_i]; + const auto *lastColorRef = reinterpret_cast(&last_target_pixel); + uint32_t last_output = blend(*lastColorRef, color); + + for (int y = min_y_i; y < max_y_i; ++y) { + for (int x = min_x_i; x < max_x_i; ++x) { + uint32_t &target_pixel = target.pixels[y * target.width + x]; + if (target_pixel == last_target_pixel) { + target_pixel = last_output; + continue; + } + last_target_pixel = target_pixel; + const auto *colorRef = reinterpret_cast(&target_pixel); + target_pixel = blend(*colorRef, color); + last_output = target_pixel; + } + } + } + + void paint_uniform_textured_rectangle(const PaintTarget &target, + const SWTexture &texture, + const ImVec4 &clip_rect, + const ImDrawVert &min_v, + const ImDrawVert &max_v) + { + const ImVec2 min_p = ImVec2(target.scale.x * min_v.pos.x, target.scale.y * min_v.pos.y); + const ImVec2 max_p = ImVec2(target.scale.x * max_v.pos.x, target.scale.y * max_v.pos.y); + + float distanceX = max_p.x - min_p.x; + float distanceY = max_p.y - min_p.y; + if (distanceX == 0 || distanceY == 0) { return; } + + // Find bounding box: + float min_x_f = min_p.x; + float min_y_f = min_p.y; + float max_x_f = max_p.x; + float max_y_f = max_p.y; + + // Clip against clip_rect: + min_x_f = std::max(min_x_f, target.scale.x * clip_rect.x - target.DisplayPos.x); + min_y_f = std::max(min_y_f, target.scale.y * clip_rect.y - target.DisplayPos.y); + max_x_f = std::min(max_x_f, target.scale.x * clip_rect.z - 0.5f - target.DisplayPos.x); + max_y_f = std::min(max_y_f, target.scale.y * clip_rect.w - 0.5f - target.DisplayPos.y); + + // Integer bounding box [min, max): + int min_x_i = static_cast(min_x_f); + int min_y_i = static_cast(min_y_f); + int max_x_i = static_cast(max_x_f + 1.0f); + int max_y_i = static_cast(max_y_f + 1.0f); + + // Clip against render target: + min_x_i = std::max(min_x_i, 0); + min_y_i = std::max(min_y_i, 0); + max_x_i = std::min(max_x_i, target.width); + max_y_i = std::min(max_y_i, target.height); + + const auto topleft = ImVec2(min_x_i + 0.5f * target.scale.x, min_y_i + 0.5f * target.scale.y); + const ImVec2 delta_uv_per_pixel = { + (max_v.uv.x - min_v.uv.x) / distanceX, + (max_v.uv.y - min_v.uv.y) / distanceY, + }; + const ImVec2 uv_topleft = { + min_v.uv.x + (topleft.x - min_v.pos.x) * delta_uv_per_pixel.x, + min_v.uv.y + (topleft.y - min_v.pos.y) * delta_uv_per_pixel.y, + }; + + int startX = uv_topleft.x * (texture.width - 1.0f) + 0.5f; + int startY = uv_topleft.y * (texture.height - 1.0f) + 0.5f; + + int currentX = startX; + int currentY = startY; + + float deltaX = delta_uv_per_pixel.x * texture.width; + float deltaY = delta_uv_per_pixel.y * texture.height; + + for (int y = min_y_i; y < max_y_i; ++y) { + currentX = startX; + for (int x = min_x_i; x < max_x_i; ++x) { + uint32_t &target_pixel = target.pixels[y * target.width + x]; + const auto *targetColorRef = reinterpret_cast(&target_pixel); + const auto *colorRef = reinterpret_cast(&min_v.col); + + if (texture.isAlpha) { + uint8_t texel = sample_font_texture(texture, currentX, currentY); + if (deltaX != 0 && currentX < texture.width - 1) { currentX += 1; } + + // The font texture is all black or all white, so optimize for this: + if (texel == 0) { continue; } + if (texel == 255) { + target_pixel = blend(*targetColorRef, *colorRef); + continue; + } + + } else { + auto texColor = sample_texture(texture, currentX, currentY); + auto src_color = reinterpret_cast(&texColor); + + if (deltaX != 0 && currentX < texture.width - 1) { currentX += 1; } + + *src_color *= *colorRef; + target_pixel = blend(*targetColorRef, *src_color); + } + } + if (deltaY != 0 && currentY < texture.height - 1) { currentY += 1; } + } + } + + // When two triangles share an edge, we want to draw the pixels on that edge exactly once. + // The edge will be the same, but the direction will be the opposite + // (assuming the two triangles have the same winding order). + // Which edge wins? This functions decides. + bool is_dominant_edge(ImVec2 edge) + { + // return edge.x < 0 || (edge.x == 0 && edge.y > 0); + return edge.y > 0 || (edge.y == 0 && edge.x < 0); + } + + // Handles triangles in any winding order (CW/CCW) + void paint_triangle(const PaintTarget &target, + const SWTexture *texture, + const ImVec4 &clip_rect, + const ImDrawVert &v0, + const ImDrawVert &v1, + const ImDrawVert &v2) + { + const ImVec2 p0 = ImVec2(target.scale.x * v0.pos.x, target.scale.y * v0.pos.y); + const ImVec2 p1 = ImVec2(target.scale.x * v1.pos.x, target.scale.y * v1.pos.y); + const ImVec2 p2 = ImVec2(target.scale.x * v2.pos.x, target.scale.y * v2.pos.y); + + const auto rect_area = barycentric(p0, p1, p2);// Can be positive or negative depending on winding order + if (rect_area == 0.0f) { return; } + // if (rect_area < 0.0f) { return paint_triangle(target, texture, clip_rect, v0, v2, v1); } + + // Find bounding box: + float min_x_f = min3(p0.x, p1.x, p2.x); + float min_y_f = min3(p0.y, p1.y, p2.y); + float max_x_f = max3(p0.x, p1.x, p2.x); + float max_y_f = max3(p0.y, p1.y, p2.y); + + // Clip against clip_rect: + min_x_f = std::max(min_x_f, target.scale.x * clip_rect.x - target.DisplayPos.x); + min_y_f = std::max(min_y_f, target.scale.y * clip_rect.y - target.DisplayPos.y); + max_x_f = std::min(max_x_f, target.scale.x * clip_rect.z - 0.5f - target.DisplayPos.x); + max_y_f = std::min(max_y_f, target.scale.y * clip_rect.w - 0.5f - target.DisplayPos.y); + + // Integer bounding box [min, max): + int min_x_i = static_cast(min_x_f); + int min_y_i = static_cast(min_y_f); + int max_x_i = static_cast(max_x_f + 1.0f); + int max_y_i = static_cast(max_y_f + 1.0f); + + // Clip against render target: + min_x_i = std::max(min_x_i, 0); + min_y_i = std::max(min_y_i, 0); + max_x_i = std::min(max_x_i, target.width); + max_y_i = std::min(max_y_i, target.height); + + // ------------------------------------------------------------------------ + // Set up interpolation of barycentric coordinates: + + const auto topleft = ImVec2(min_x_i + 0.5f * target.scale.x, min_y_i + 0.5f * target.scale.y); + const auto dx = ImVec2(1, 0); + const auto dy = ImVec2(0, 1); + + const auto w0_topleft = barycentric(p1, p2, topleft); + const auto w1_topleft = barycentric(p2, p0, topleft); + const auto w2_topleft = barycentric(p0, p1, topleft); + + const auto w0_dx = barycentric(p1, p2, topleft + dx) - w0_topleft; + const auto w1_dx = barycentric(p2, p0, topleft + dx) - w1_topleft; + const auto w2_dx = barycentric(p0, p1, topleft + dx) - w2_topleft; + + const auto w0_dy = barycentric(p1, p2, topleft + dy) - w0_topleft; + const auto w1_dy = barycentric(p2, p0, topleft + dy) - w1_topleft; + const auto w2_dy = barycentric(p0, p1, topleft + dy) - w2_topleft; + + const Barycentric bary_0{ 1, 0, 0 }; + const Barycentric bary_1{ 0, 1, 0 }; + const Barycentric bary_2{ 0, 0, 1 }; + + const auto inv_area = 1 / rect_area; + const Barycentric bary_topleft = inv_area * (w0_topleft * bary_0 + w1_topleft * bary_1 + w2_topleft * bary_2); + const Barycentric bary_dx = inv_area * (w0_dx * bary_0 + w1_dx * bary_1 + w2_dx * bary_2); + const Barycentric bary_dy = inv_area * (w0_dy * bary_0 + w1_dy * bary_1 + w2_dy * bary_2); + + Barycentric bary_current_row = bary_topleft; + + // ------------------------------------------------------------------------ + // For pixel-perfect inside/outside testing: + + const int sign = rect_area > 0 ? 1 : -1;// winding order? + + const int bias0i = is_dominant_edge(p2 - p1) ? 0 : -1; + const int bias1i = is_dominant_edge(p0 - p2) ? 0 : -1; + const int bias2i = is_dominant_edge(p1 - p0) ? 0 : -1; + + const auto p0i = as_point(p0); + const auto p1i = as_point(p1); + const auto p2i = as_point(p2); + + // ------------------------------------------------------------------------ + + const bool has_uniform_color = (v0.col == v1.col && v0.col == v2.col); + + const ImVec4 c0 = color_convert_u32_to_float4(v0.col); + const ImVec4 c1 = color_convert_u32_to_float4(v1.col); + const ImVec4 c2 = color_convert_u32_to_float4(v2.col); + + // We often blend the same colors over and over again, so optimize for this (saves 10% total cpu): + uint32_t last_target_pixel = 0; + const auto *lastColorRef = reinterpret_cast(&last_target_pixel); + const auto *colorRef = reinterpret_cast(&v0.col); + uint32_t last_output = blend(*lastColorRef, *colorRef); + + for (int y = min_y_i; y < max_y_i; ++y) { + auto bary = bary_current_row; + + bool has_been_inside_this_row = false; + + for (int x = min_x_i; x < max_x_i; ++x) { + const auto w0 = bary.w0; + const auto w1 = bary.w1; + const auto w2 = bary.w2; + bary += bary_dx; + + { + // Inside/outside test: + const auto p = Point{ kFixedBias * x + kFixedBias / 2, kFixedBias * y + kFixedBias / 2 }; + const auto w0i = sign * orient2d(p1i, p2i, p) + bias0i; + const auto w1i = sign * orient2d(p2i, p0i, p) + bias1i; + const auto w2i = sign * orient2d(p0i, p1i, p) + bias2i; + if (w0i < 0 || w1i < 0 || w2i < 0) { + if (has_been_inside_this_row) { + break;// Gives a nice 10% speedup + } else { + continue; + } + } + } + has_been_inside_this_row = true; + + uint32_t &target_pixel = target.pixels[y * target.width + x]; + + if (has_uniform_color && !texture) { + if (target_pixel == last_target_pixel) { + target_pixel = last_output; + continue; + } + last_target_pixel = target_pixel; + target_pixel = blend(*lastColorRef, *colorRef); + last_output = target_pixel; + continue; + } + + ImVec4 src_color; + + if (has_uniform_color) { + src_color = c0; + } else { + src_color = w0 * c0 + w1 * c1 + w2 * c2; + } + + if (texture) { + if (!texture->isAlpha) { printf("warning: different texture\n"); } + + const ImVec2 uv = w0 * v0.uv + w1 * v1.uv + w2 * v2.uv; + int x = uv.x * (texture->width - 1.0f) + 0.5f; + int y = uv.y * (texture->height - 1.0f) + 0.5f; + src_color.w *= sample_font_texture(*texture, x, y) / 255.0f; + } + + if (src_color.w <= 0.0f) { continue; }// Transparent. + if (src_color.w >= 1.0f) { + // Opaque, no blending needed: + target_pixel = color_convert_float4_to_u32(src_color); + continue; + } + + ImVec4 target_color = color_convert_u32_to_float4(target_pixel); + const auto blended_color = src_color.w * src_color + (1.0f - src_color.w) * target_color; + target_pixel = color_convert_float4_to_u32(blended_color); + } + + bary_current_row += bary_dy; + } + } + + void paint_draw_cmd(const PaintTarget &target, + const ImDrawVert *vertices, + const ImDrawIdx *idx_buffer, + const ImDrawCmd &pcmd, + const SwOptions &options) + { + const auto texture = reinterpret_cast(pcmd.TextureId); + IM_ASSERT(texture); + + // ImGui uses the first pixel for "white". + const ImVec2 white_uv = ImVec2(0.5f / texture->width, 0.5f / texture->height); + + for (unsigned int i = 0; i + 3 <= pcmd.ElemCount;) { + ImDrawVert v0 = vertices[idx_buffer[i + 0]]; + v0.pos.x -= target.DisplayPos.x; + v0.pos.y -= target.DisplayPos.y; + ImDrawVert v1 = vertices[idx_buffer[i + 1]]; + v1.pos.x -= target.DisplayPos.x; + v1.pos.y -= target.DisplayPos.y; + ImDrawVert v2 = vertices[idx_buffer[i + 2]]; + v2.pos.x -= target.DisplayPos.x; + v2.pos.y -= target.DisplayPos.y; + + // Text is common, and is made of textured rectangles. So let's optimize for it. + // This assumes the ImGui way to layout text does not change. + if (options.optimize_text && i + 6 <= pcmd.ElemCount && idx_buffer[i + 3] == idx_buffer[i + 0] + && idx_buffer[i + 4] == idx_buffer[i + 2]) { + ImDrawVert v3 = vertices[idx_buffer[i + 5]]; + v3.pos.x -= target.DisplayPos.x; + v3.pos.y -= target.DisplayPos.y; + + if (v0.pos.x == v3.pos.x && v1.pos.x == v2.pos.x && v0.pos.y == v1.pos.y && v2.pos.y == v3.pos.y + && v0.uv.x == v3.uv.x && v1.uv.x == v2.uv.x && v0.uv.y == v1.uv.y && v2.uv.y == v3.uv.y) { + const bool has_uniform_color = v0.col == v1.col && v0.col == v2.col && v0.col == v3.col; + + const bool has_texture = v0.uv != white_uv || v1.uv != white_uv || v2.uv != white_uv || v3.uv != white_uv; + + if (has_uniform_color && has_texture) { + paint_uniform_textured_rectangle(target, *texture, pcmd.ClipRect, v0, v2); + i += 6; + continue; + } + } + } + + // A lot of the big stuff are uniformly colored rectangles, + // so we can save a lot of CPU by detecting them: + if (options.optimize_rectangles && i + 6 <= pcmd.ElemCount) { + ImDrawVert v3 = vertices[idx_buffer[i + 3]]; + v3.pos.x -= target.DisplayPos.x; + v3.pos.y -= target.DisplayPos.y; + ImDrawVert v4 = vertices[idx_buffer[i + 4]]; + v4.pos.x -= target.DisplayPos.x; + v4.pos.y -= target.DisplayPos.y; + ImDrawVert v5 = vertices[idx_buffer[i + 5]]; + v5.pos.x -= target.DisplayPos.x; + v5.pos.y -= target.DisplayPos.y; + + ImVec2 min, max; + min.x = min3(v0.pos.x - target.DisplayPos.x, v1.pos.x - target.DisplayPos.x, v2.pos.x - target.DisplayPos.x); + min.y = min3(v0.pos.y - target.DisplayPos.y, v1.pos.y - target.DisplayPos.y, v2.pos.y - target.DisplayPos.y); + max.x = max3(v0.pos.x - target.DisplayPos.x, v1.pos.x - target.DisplayPos.x, v2.pos.x - target.DisplayPos.x); + max.y = max3(v0.pos.y - target.DisplayPos.y, v1.pos.y - target.DisplayPos.y, v2.pos.y - target.DisplayPos.y); + + // Not the prettiest way to do this, but it catches all cases + // of a rectangle split into two triangles. + // TODO: Stop it from also assuming duplicate triangles is one rectangle. + if ((v0.pos.x == min.x || v0.pos.x == max.x) && (v0.pos.y == min.y || v0.pos.y == max.y) + && (v1.pos.x == min.x || v1.pos.x == max.x) && (v1.pos.y == min.y || v1.pos.y == max.y) + && (v2.pos.x == min.x || v2.pos.x == max.x) && (v2.pos.y == min.y || v2.pos.y == max.y) + && (v3.pos.x == min.x || v3.pos.x == max.x) && (v3.pos.y == min.y || v3.pos.y == max.y) + && (v4.pos.x == min.x || v4.pos.x == max.x) && (v4.pos.y == min.y || v4.pos.y == max.y) + && (v5.pos.x == min.x || v5.pos.x == max.x) && (v5.pos.y == min.y || v5.pos.y == max.y)) { + const bool has_uniform_color = + v0.col == v1.col && v0.col == v2.col && v0.col == v3.col && v0.col == v4.col && v0.col == v5.col; + + min.x = std::max(min.x, pcmd.ClipRect.x - target.DisplayPos.x); + min.y = std::max(min.y, pcmd.ClipRect.y - target.DisplayPos.y); + max.x = std::min(max.x, pcmd.ClipRect.z - 0.5f - target.DisplayPos.x); + max.y = std::min(max.y, pcmd.ClipRect.w - 0.5f - target.DisplayPos.y); + + if (max.x < min.x || max.y < min.y) { + i += 6; + continue; + }// Completely clipped + + if (has_uniform_color) { + const auto *colorRef = reinterpret_cast(&v0.col); + paint_uniform_rectangle(target, min, max, *colorRef); + i += 6; + continue; + } + } + } + + const bool has_texture = (v0.uv != white_uv || v1.uv != white_uv || v2.uv != white_uv); + paint_triangle(target, has_texture ? texture : nullptr, pcmd.ClipRect, v0, v1, v2); + i += 3; + } + } + + void paint_draw_list(const PaintTarget &target, const ImDrawList *cmd_list, const SwOptions &options) + { + const ImDrawIdx *idx_buffer = &cmd_list->IdxBuffer[0]; + const ImDrawVert *vertices = cmd_list->VtxBuffer.Data; + + for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.size(); cmd_i++) { + const ImDrawCmd &pcmd = cmd_list->CmdBuffer[cmd_i]; + if (pcmd.UserCallback) { + pcmd.UserCallback(cmd_list, &pcmd); + } else { + paint_draw_cmd(target, vertices, idx_buffer, pcmd, options); + } + idx_buffer += pcmd.ElemCount; + } + } + +}// namespace + +void paint_imgui(uint32_t *pixels, ImDrawData *drawData, const SwOptions &options = {}) +{ + int fb_width = (int)(drawData->DisplaySize.x * drawData->FramebufferScale.x); + int fb_height = (int)(drawData->DisplaySize.y * drawData->FramebufferScale.y); + if (fb_width <= 0 || fb_height <= 0) return; + + PaintTarget target{ pixels, fb_width, fb_height, drawData->FramebufferScale, drawData->DisplayPos }; + + for (int i = 0; i < drawData->CmdListsCount; ++i) { + paint_draw_list(target, drawData->CmdLists[i], options); + } +} + +/// NEW STUFF + +bool ImGui_ImplSW_Init(SDL_Window* win) { + ImGuiIO& io = ImGui::GetIO(); + IM_ASSERT(io.BackendRendererUserData == nullptr); + + if (SDL_HasWindowSurface(win)==SDL_FALSE) { + return false; + } + + ImGui_ImplSW_Data* bd = IM_NEW(ImGui_ImplSW_Data)(); + bd->Window = win; + io.BackendRendererUserData = (void*)bd; + io.BackendRendererName = "imgui_sw"; + + return true; +} + +void ImGui_ImplSW_Shutdown() { + ImGui_ImplSW_Data* bd = ImGui_ImplSW_GetBackendData(); + IM_ASSERT(bd != nullptr); + ImGuiIO& io = ImGui::GetIO(); + + ImGui_ImplSW_DestroyDeviceObjects(); + io.BackendRendererName = nullptr; + io.BackendRendererUserData = nullptr; + IM_DELETE(bd); +} + +bool ImGui_ImplSW_NewFrame() { + ImGui_ImplSW_Data* bd = ImGui_ImplSW_GetBackendData(); + IM_ASSERT(bd != nullptr); + + if (!bd->FontTexture) ImGui_ImplSW_CreateDeviceObjects(); + + return true; +} + +void ImGui_ImplSW_RenderDrawData(ImDrawData* draw_data) { + ImGui_ImplSW_Data* bd = ImGui_ImplSW_GetBackendData(); + IM_ASSERT(bd != nullptr); + + SDL_Surface* surf = SDL_GetWindowSurface(bd->Window); + if (!surf) return; + + bool mustLock=SDL_MUSTLOCK(surf); + if (mustLock) { + if (SDL_LockSurface(surf)!=0) return; + } + paint_imgui((uint32_t*)surf->pixels,draw_data); + if (mustLock) { + SDL_UnlockSurface(surf); + } +} + +/// CREATE OBJECTS + +bool ImGui_ImplSW_CreateFontsTexture() { + ImGuiIO &io = ImGui::GetIO(); + ImGui_ImplSW_Data* bd = ImGui_ImplSW_GetBackendData(); + + // Load default font (embedded in code): + uint8_t *tex_data; + int font_width, font_height; + io.Fonts->GetTexDataAsAlpha8(&tex_data, &font_width, &font_height); + SWTexture* texture = new SWTexture((uint32_t*)tex_data,font_width,font_height,true); + io.Fonts->SetTexID(texture); + bd->FontTexture = texture; + + return true; +} + +void ImGui_ImplSW_DestroyFontsTexture() { + ImGuiIO& io = ImGui::GetIO(); + ImGui_ImplSW_Data* bd = ImGui_ImplSW_GetBackendData(); + if (bd->FontTexture) + { + delete bd->FontTexture; + io.Fonts->SetTexID(0); + bd->FontTexture = 0; + } +} + +bool ImGui_ImplSW_CreateDeviceObjects() { + return ImGui_ImplSW_CreateFontsTexture(); +} + +void ImGui_ImplSW_DestroyDeviceObjects() { + ImGui_ImplSW_DestroyFontsTexture(); +} + +#endif // #ifndef IMGUI_DISABLE diff --git a/extern/imgui_software_renderer/imgui_sw.hpp b/extern/imgui_software_renderer/imgui_sw.hpp new file mode 100644 index 000000000..a3767e097 --- /dev/null +++ b/extern/imgui_software_renderer/imgui_sw.hpp @@ -0,0 +1,58 @@ +// By Emil Ernerfeldt 2018 +// LICENSE: +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// WHAT: +// This is a software renderer for Dear ImGui. +// It is decently fast, but has a lot of room for optimization. +// The goal was to get something fast and decently accurate in not too many lines of code. +// LIMITATIONS: +// * It is not pixel-perfect, but it is good enough for must use cases. +// * It does not support painting with any other texture than the default font texture. +#pragma once +#include "imgui.h" // IMGUI_IMPL_API +#ifndef IMGUI_DISABLE + +#include + +struct SDL_Window; +struct ImDrawData; + +struct SWTexture +{ + uint32_t* pixels; + int width; + int height; + bool managed, isAlpha; + + SWTexture(uint32_t* pix, int w, int h, bool a=false): + pixels(pix), + width(w), + height(h), + managed(false), + isAlpha(a) {} + SWTexture(int w, int h, bool a=false): + width(w), + height(h), + managed(true), + isAlpha(a) { + pixels=new uint32_t[width*height]; + } + ~SWTexture() { + if (managed) delete[] pixels; + } +}; + +IMGUI_IMPL_API bool ImGui_ImplSW_Init(SDL_Window* win); +IMGUI_IMPL_API void ImGui_ImplSW_Shutdown(); +IMGUI_IMPL_API bool ImGui_ImplSW_NewFrame(); +IMGUI_IMPL_API void ImGui_ImplSW_RenderDrawData(ImDrawData* draw_data); + +// Called by Init/NewFrame/Shutdown +IMGUI_IMPL_API bool ImGui_ImplSW_CreateFontsTexture(); +IMGUI_IMPL_API void ImGui_ImplSW_DestroyFontsTexture(); +IMGUI_IMPL_API bool ImGui_ImplSW_CreateDeviceObjects(); +IMGUI_IMPL_API void ImGui_ImplSW_DestroyDeviceObjects(); + +#endif // #ifndef IMGUI_DISABLE diff --git a/src/gui/gui.h b/src/gui/gui.h index 4f24523b3..41cc28128 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -74,7 +74,8 @@ enum FurnaceGUIRenderBackend { GUI_BACKEND_GL2, GUI_BACKEND_GL1, GUI_BACKEND_DX11, - GUI_BACKEND_DX9 + GUI_BACKEND_DX9, + GUI_BACKEND_SOFTWARE }; #ifdef HAVE_RENDER_DX11 @@ -99,7 +100,8 @@ enum FurnaceGUIRenderBackend { #define GUI_BACKEND_DEFAULT GUI_BACKEND_SDL #define GUI_BACKEND_DEFAULT_NAME "SDL" #else -#error how did you manage to do that? +#define GUI_BACKEND_DEFAULT GUI_BACKEND_SOFTWARE +#define GUI_BACKEDN_DEFAULT_NAME "Software" #endif #endif #endif diff --git a/src/gui/render.cpp b/src/gui/render.cpp index 5472f8331..60e0c83c6 100644 --- a/src/gui/render.cpp +++ b/src/gui/render.cpp @@ -31,6 +31,7 @@ #ifdef HAVE_RENDER_DX11 #include "render/renderDX11.h" #endif +#include "render/renderSoftware.h" bool FurnaceGUI::initRender() { if (rend!=NULL) return false; @@ -49,6 +50,8 @@ bool FurnaceGUI::initRender() { renderBackend=GUI_BACKEND_DX9; } else if (settings.renderBackend=="SDL") { renderBackend=GUI_BACKEND_SDL; + } else if (settings.renderBackend=="Software") { + renderBackend=GUI_BACKEND_SOFTWARE; } else { renderBackend=GUI_BACKEND_DEFAULT; } @@ -99,6 +102,10 @@ bool FurnaceGUI::initRender() { rend=new FurnaceGUIRenderSDL; break; #endif + case GUI_BACKEND_SOFTWARE: + logI("render backend: Software"); + rend=new FurnaceGUIRenderSoftware; + break; default: logE("invalid render backend!"); return false; diff --git a/src/gui/render/renderSoftware.cpp b/src/gui/render/renderSoftware.cpp new file mode 100644 index 000000000..60dbd554e --- /dev/null +++ b/src/gui/render/renderSoftware.cpp @@ -0,0 +1,144 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2024 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "renderSoftware.h" +#include "imgui_sw.hpp" +#include "../../ta-log.h" + +class FurnaceSoftwareTexture: public FurnaceGUITexture { + public: + SWTexture* tex; + FurnaceSoftwareTexture(): + tex(NULL) {} +}; + +ImTextureID FurnaceGUIRenderSoftware::getTextureID(FurnaceGUITexture* which) { + FurnaceSoftwareTexture* t=(FurnaceSoftwareTexture*)which; + return t->tex; +} + +bool FurnaceGUIRenderSoftware::lockTexture(FurnaceGUITexture* which, void** data, int* pitch) { + FurnaceSoftwareTexture* t=(FurnaceSoftwareTexture*)which; + if (!t->tex->managed) return false; + *data=t->tex->pixels; + *pitch=t->tex->width*(t->tex->isAlpha?1:4); + return true; +} + +bool FurnaceGUIRenderSoftware::unlockTexture(FurnaceGUITexture* which) { + return true; +} + +bool FurnaceGUIRenderSoftware::updateTexture(FurnaceGUITexture* which, void* data, int pitch) { + FurnaceSoftwareTexture* t=(FurnaceSoftwareTexture*)which; + if (!t->tex->managed) return false; + memcpy(t->tex->pixels,data,pitch*t->tex->height); + return true; +} + +FurnaceGUITexture* FurnaceGUIRenderSoftware::createTexture(bool dynamic, int width, int height, bool interpolate) { + FurnaceSoftwareTexture* ret=new FurnaceSoftwareTexture; + ret->tex=new SWTexture(width,height); + return ret; +} + +bool FurnaceGUIRenderSoftware::destroyTexture(FurnaceGUITexture* which) { + FurnaceSoftwareTexture* t=(FurnaceSoftwareTexture*)which; + + delete t->tex; + delete t; + return true; +} + +void FurnaceGUIRenderSoftware::setTextureBlendMode(FurnaceGUITexture* which, FurnaceGUIBlendMode mode) { + // TODO +} + +void FurnaceGUIRenderSoftware::setBlendMode(FurnaceGUIBlendMode mode) { + // TODO +} + +void FurnaceGUIRenderSoftware::clear(ImVec4 color) { + // TODO +} + +bool FurnaceGUIRenderSoftware::newFrame() { + return ImGui_ImplSW_NewFrame(); +} + +bool FurnaceGUIRenderSoftware::canVSync() { + return false; +} + +void FurnaceGUIRenderSoftware::createFontsTexture() { + ImGui_ImplSW_CreateFontsTexture(); +} + +void FurnaceGUIRenderSoftware::destroyFontsTexture() { + ImGui_ImplSW_DestroyFontsTexture(); +} + +void FurnaceGUIRenderSoftware::renderGUI() { + ImGui_ImplSW_RenderDrawData(ImGui::GetDrawData()); +} + +void FurnaceGUIRenderSoftware::wipe(float alpha) { + // TODO +} + +void FurnaceGUIRenderSoftware::present() { + // TODO? +} + +bool FurnaceGUIRenderSoftware::getOutputSize(int& w, int& h) { + SDL_Surface* surf=SDL_GetWindowSurface(sdlWin); + if (surf==NULL) return false; + w=surf->w; + h=surf->h; + return true; +} + +int FurnaceGUIRenderSoftware::getWindowFlags() { + return 0; +} + +void FurnaceGUIRenderSoftware::setSwapInterval(int swapInterval) { +} + +void FurnaceGUIRenderSoftware::preInit() { +} + +bool FurnaceGUIRenderSoftware::init(SDL_Window* win, int swapInterval) { + sdlWin=win; + return true; +} + +void FurnaceGUIRenderSoftware::initGUI(SDL_Window* win) { + // hack + ImGui_ImplSDL2_InitForMetal(win); + ImGui_ImplSW_Init(win); +} + +void FurnaceGUIRenderSoftware::quitGUI() { + ImGui_ImplSW_Shutdown(); +} + +bool FurnaceGUIRenderSoftware::quit() { + return true; +} diff --git a/src/gui/render/renderSoftware.h b/src/gui/render/renderSoftware.h new file mode 100644 index 000000000..7d96883e7 --- /dev/null +++ b/src/gui/render/renderSoftware.h @@ -0,0 +1,51 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2024 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "../gui.h" + +class FurnaceGUIRenderSoftware: public FurnaceGUIRender { + SDL_Window* sdlWin; + public: + ImTextureID getTextureID(FurnaceGUITexture* which); + bool lockTexture(FurnaceGUITexture* which, void** data, int* pitch); + bool unlockTexture(FurnaceGUITexture* which); + bool updateTexture(FurnaceGUITexture* which, void* data, int pitch); + FurnaceGUITexture* createTexture(bool dynamic, int width, int height, bool interpolate=true); + bool destroyTexture(FurnaceGUITexture* which); + void setTextureBlendMode(FurnaceGUITexture* which, FurnaceGUIBlendMode mode); + void setBlendMode(FurnaceGUIBlendMode mode); + void clear(ImVec4 color); + bool newFrame(); + bool canVSync(); + void createFontsTexture(); + void destroyFontsTexture(); + void renderGUI(); + void wipe(float alpha); + void present(); + bool getOutputSize(int& w, int& h); + int getWindowFlags(); + void setSwapInterval(int swapInterval); + void preInit(); + bool init(SDL_Window* win, int swapInterval); + void initGUI(SDL_Window* win); + void quitGUI(); + bool quit(); + FurnaceGUIRenderSoftware(): + sdlWin(NULL) {} +}; diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index d93737781..5027809cc 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -428,6 +428,10 @@ void FurnaceGUI::drawSettings() { settingsChanged=true; } #endif + if (ImGui::Selectable("Software",curRenderBackend=="Software")) { + settings.renderBackend="Software"; + settingsChanged=true; + } ImGui::EndCombo(); } if (ImGui::IsItemHovered()) {