// Original File By Emil Ernerfeldt 2018 // https://github.com/emilk/imgui_software_renderer // 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 "impl_sw.h" #include #include #include #include "external/imgui/imgui.h" namespace imgui_sw { namespace { struct Texture { const uint8_t* pixels; // 8-bit. int width; int height; }; struct PaintTarget { uint32_t* pixels; int width; int height; ImVec2 scale; // Multiply ImGui (point) coordinates with this to get pixel coordinates. }; // ---------------------------------------------------------------------------- struct ColorInt { uint32_t a, b, g, r; ColorInt() = default; explicit ColorInt(uint32_t x) { a = (x >> IM_COL32_A_SHIFT) & 0xFFu; b = (x >> IM_COL32_B_SHIFT) & 0xFFu; g = (x >> IM_COL32_G_SHIFT) & 0xFFu; r = (x >> IM_COL32_R_SHIFT) & 0xFFu; } uint32_t toUint32() const { #ifdef IMGUI_USE_BGRA_PACKED_COLOR return (a << 24u) | (r << 16u) | (g << 8u) | b; #else return (a << 24u) | (b << 16u) | (g << 8u) | r; #endif } }; ColorInt blend(ColorInt target, ColorInt source) { ColorInt result; result.a = 0; // Whatever. result.b = (source.b * source.a + target.b * (255 - source.a)) / 255; result.g = (source.g * source.a + target.g * (255 - source.a)) / 255; result.r = (source.r * source.a + target.r * (255 - source.a)) / 255; return result; } // ---------------------------------------------------------------------------- // 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}; } ImVec2 operator+(const ImVec2& a, const ImVec2& b) { return ImVec2{a.x + b.x, a.y + b.y}; } ImVec2 operator-(const ImVec2& a, const ImVec2& b) { return ImVec2{a.x - b.x, a.y - b.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}; } ImVec4 operator+(const ImVec4& a, const ImVec4& b) { return ImVec4{a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.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. 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(std::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_texture(const Texture& texture, const ImVec2& uv) { int tx = static_cast(uv.x * (texture.width - 1.0f) + 0.5f); int ty = static_cast(uv.y * (texture.height - 1.0f) + 0.5f); // Clamp to inside of texture: tx = std::max(tx, 0); tx = std::min(tx, texture.width - 1); ty = std::max(ty, 0); ty = std::min(ty, texture.height - 1); return texture.pixels[ty * texture.width + tx]; } void paint_uniform_rectangle( const PaintTarget& target, const ImVec2& min_f, const ImVec2& max_f, const ColorInt& color, Stats* stats) { // 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); stats->uniform_rectangle_pixels += (max_x_i - min_x_i) * (max_y_i - min_y_i); // 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]; uint32_t last_output = blend(ColorInt(last_target_pixel), color).toUint32(); 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; target_pixel = blend(ColorInt(target_pixel), color).toUint32(); last_output = target_pixel; } } } void paint_uniform_textured_rectangle( const PaintTarget& target, const Texture& texture, const ImVec4& clip_rect, const ImDrawVert& min_v, const ImDrawVert& max_v, Stats* stats) { 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); // 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); min_y_f = std::max(min_y_f, target.scale.y * clip_rect.y); max_x_f = std::min(max_x_f, target.scale.x * clip_rect.z - 0.5f); max_y_f = std::min(max_y_f, target.scale.y * clip_rect.w - 0.5f); // 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); stats->font_pixels += (max_x_i - min_x_i) * (max_y_i - min_y_i); 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) / (max_p.x - min_p.x), (max_v.uv.y - min_v.uv.y) / (max_p.y - min_p.y), }; 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, }; ImVec2 current_uv = uv_topleft; for (int y = min_y_i; y < max_y_i; ++y, current_uv.y += delta_uv_per_pixel.y) { current_uv.x = uv_topleft.x; for (int x = min_x_i; x < max_x_i; ++x, current_uv.x += delta_uv_per_pixel.x) { uint32_t& target_pixel = target.pixels[y * target.width + x]; const uint8_t texel = sample_texture(texture, current_uv); // The font texture is all black or all white, so optimize for this: if (texel == 0) { continue; } // Other textured rectangles ColorInt source_color = ColorInt(min_v.col); source_color.a = source_color.a * texel / 255; target_pixel = blend(ColorInt(target_pixel), source_color).toUint32(); } } } // 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 Texture* texture, const ImVec4& clip_rect, const ImDrawVert& v0, const ImDrawVert& v1, const ImDrawVert& v2, Stats* stats) { 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, stats); } // 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); min_y_f = std::max(min_y_f, target.scale.y * clip_rect.y); max_x_f = std::min(max_x_f, target.scale.x * clip_rect.z - 0.5f); max_y_f = std::min(max_y_f, target.scale.y * clip_rect.w - 0.5f); // 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; uint32_t last_output = blend(ColorInt(last_target_pixel), ColorInt(v0.col)).toUint32(); 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) { stats->uniform_triangle_pixels += 1; if (target_pixel == last_target_pixel) { target_pixel = last_output; continue; } last_target_pixel = target_pixel; target_pixel = blend(ColorInt(target_pixel), ColorInt(v0.col)).toUint32(); last_output = target_pixel; continue; } ImVec4 src_color; if (has_uniform_color) { src_color = c0; } else { stats->gradient_triangle_pixels += 1; src_color = w0 * c0 + w1 * c1 + w2 * c2; } if (texture) { stats->textured_triangle_pixels += 1; const ImVec2 uv = w0 * v0.uv + w1 * v1.uv + w2 * v2.uv; src_color.w *= sample_texture(*texture, uv) / 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, Stats* stats) { const auto texture = reinterpret_cast(pcmd.TextureId); const auto offset = pcmd.IdxOffset; // ImGui uses the first pixel for "white". const ImVec2 white_uv = ImVec2(0.5f / texture->width, 0.5f / texture->height); for (size_t i = 0; i + 3 <= pcmd.ElemCount; ) { const auto io = i + offset; const ImDrawVert& v0 = vertices[idx_buffer[io + 0]]; const ImDrawVert& v1 = vertices[idx_buffer[io + 1]]; const ImDrawVert& v2 = vertices[idx_buffer[io + 2]]; // 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[io + 3] == idx_buffer[io + 0] && idx_buffer[io + 4] == idx_buffer[io + 2]) { const ImDrawVert& v3 = vertices[idx_buffer[io + 5]]; 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, stats); 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) { const ImDrawVert& v3 = vertices[idx_buffer[io + 3]]; const ImDrawVert& v4 = vertices[idx_buffer[io + 4]]; const ImDrawVert& v5 = vertices[idx_buffer[io + 5]]; ImVec2 min, max; min.x = min3(v0.pos.x, v1.pos.x, v2.pos.x); min.y = min3(v0.pos.y, v1.pos.y, v2.pos.y); max.x = max3(v0.pos.x, v1.pos.x, v2.pos.x); max.y = max3(v0.pos.y, v1.pos.y, v2.pos.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; const bool has_texture = v0.uv != white_uv || v1.uv != white_uv || v2.uv != white_uv || v3.uv != white_uv || v4.uv != white_uv || v5.uv != white_uv; min.x = std::max(min.x, pcmd.ClipRect.x); min.y = std::max(min.y, pcmd.ClipRect.y); max.x = std::min(max.x, pcmd.ClipRect.z - 0.5f); max.y = std::min(max.y, pcmd.ClipRect.w - 0.5f); if (max.x < min.x || max.y < min.y) { i += 6; continue; } // Completely clipped const auto num_pixels = (max.x - min.x) * (max.y - min.y) * target.scale.x * target.scale.y; if (has_uniform_color) { if (has_texture) { stats->textured_rectangle_pixels += num_pixels; } else { paint_uniform_rectangle(target, min, max, ColorInt(v0.col), stats); i += 6; continue; } } else { if (has_texture) { // I have never encountered these. stats->gradient_textured_rectangle_pixels += num_pixels; } else { // Color picker. TODO: Optimize stats->gradient_rectangle_pixels += num_pixels; } } } } 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, stats); i += 3; } } void paint_draw_list(const PaintTarget& target, const ImDrawList* cmd_list, const SwOptions& options, Stats* stats) { 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, stats); } } } } // namespace void make_style_fast() { ImGuiStyle& style = ImGui::GetStyle(); style.AntiAliasedLines = false; style.AntiAliasedFill = false; style.WindowRounding = 0; } void restore_style() { ImGuiStyle& style = ImGui::GetStyle(); const ImGuiStyle default_style = ImGuiStyle(); style.AntiAliasedLines = default_style.AntiAliasedLines; style.AntiAliasedFill = default_style.AntiAliasedFill; style.WindowRounding = default_style.WindowRounding; } void bind_imgui_painting() { // make sure it doesn't get called twice static bool done = false; if (done) { return; } done = true; // Load default font (embedded in code): ImGuiIO& io = ImGui::GetIO(); uint8_t* tex_data; int font_width, font_height; io.Fonts->GetTexDataAsAlpha8(&tex_data, &font_width, &font_height); const auto texture = new Texture{tex_data, font_width, font_height}; io.Fonts->TexID = texture; } static Stats s_stats; // TODO: pass as an argument? void paint_imgui(uint32_t* pixels, int width_pixels, int height_pixels, const SwOptions& options) { const float width_points = ImGui::GetIO().DisplaySize.x; const float height_points = ImGui::GetIO().DisplaySize.y; const ImVec2 scale{width_pixels / width_points, height_pixels / height_points}; PaintTarget target{pixels, width_pixels, height_pixels, scale}; const ImDrawData* draw_data = ImGui::GetDrawData(); s_stats = Stats{}; for (int i = 0; i < draw_data->CmdListsCount; ++i) { paint_draw_list(target, draw_data->CmdLists[i], options, &s_stats); } } void unbind_imgui_painting() { ImGuiIO& io = ImGui::GetIO(); delete reinterpret_cast(io.Fonts->TexID); io.Fonts = nullptr; } bool show_options(SwOptions* io_options) { bool changed = false; changed |= ImGui::Checkbox("optimize_text", &io_options->optimize_text); changed |= ImGui::Checkbox("optimize_rectangles", &io_options->optimize_rectangles); return changed; } void show_stats() { ImGui::Text("uniform_triangle_pixels: %7d", s_stats.uniform_triangle_pixels); ImGui::Text("textured_triangle_pixels: %7d", s_stats.textured_triangle_pixels); ImGui::Text("gradient_triangle_pixels: %7d", s_stats.gradient_triangle_pixels); ImGui::Text("font_pixels: %7d", s_stats.font_pixels); ImGui::Text("uniform_rectangle_pixels: %7.0f", s_stats.uniform_rectangle_pixels); ImGui::Text("textured_rectangle_pixels: %7.0f", s_stats.textured_rectangle_pixels); ImGui::Text("gradient_rectangle_pixels: %7.0f", s_stats.gradient_rectangle_pixels); ImGui::Text("gradient_textured_rectangle_pixels: %7.0f", s_stats.gradient_textured_rectangle_pixels); } Stats get_stats() { return s_stats; } } // namespace imgui_sw