Files
WickedEngine/WickedEngine/wiProfiler.cpp
T
2024-05-27 10:00:52 +02:00

675 lines
21 KiB
C++

#include "wiProfiler.h"
#include "wiGraphicsDevice.h"
#include "wiFont.h"
#include "wiImage.h"
#include "wiTimer.h"
#include "wiTextureHelper.h"
#include "wiHelper.h"
#include "wiUnorderedMap.h"
#include "wiBacklog.h"
#include "wiRenderer.h"
#include "wiEventHandler.h"
#if __has_include("Superluminal/PerformanceAPI_capi.h")
#include "Superluminal/PerformanceAPI_capi.h"
#include "Superluminal/PerformanceAPI_loader.h"
#endif // superluminal
#include <string>
#include <stack>
#include <mutex>
#include <atomic>
#include <sstream>
using namespace wi::graphics;
namespace wi::profiler
{
bool ENABLED = false;
bool ENABLED_REQUEST = false;
bool initialized = false;
std::mutex lock;
range_id cpu_frame;
range_id gpu_frame;
GPUQueryHeap queryHeap;
GPUBuffer queryResultBuffer[GraphicsDevice::GetBufferCount()];
std::atomic<uint32_t> nextQuery{ 0 };
uint32_t queryheap_idx = 0;
bool drawn_this_frame = false;
wi::Color background_color = wi::Color(20, 20, 20, 230);
wi::Color text_color = wi::Color::White();
#if PERFORMANCEAPI_ENABLED
PerformanceAPI_ModuleHandle superluminal_handle = {};
PerformanceAPI_Functions superluminal_functions = {};
#endif // PERFORMANCEAPI_ENABLED
struct Range
{
bool in_use = false;
std::string name;
float times[20] = {};
int avg_counter = 0;
float time = 0;
CommandList cmd;
wi::Timer cpuTimer;
int gpuBegin[arraysize(queryResultBuffer)];
int gpuEnd[arraysize(queryResultBuffer)];
bool IsCPURange() const { return !cmd.IsValid(); }
};
wi::unordered_map<size_t, Range> ranges;
void BeginFrame()
{
if (ENABLED_REQUEST != ENABLED)
{
ranges.clear();
ENABLED = ENABLED_REQUEST;
}
if (!ENABLED)
return;
if (!initialized)
{
initialized = true;
ranges.reserve(100);
GraphicsDevice* device = wi::graphics::GetDevice();
GPUQueryHeapDesc desc;
desc.type = GpuQueryType::TIMESTAMP;
desc.query_count = 1024;
bool success = device->CreateQueryHeap(&desc, &queryHeap);
assert(success);
GPUBufferDesc bd;
bd.usage = Usage::READBACK;
bd.size = desc.query_count * sizeof(uint64_t);
for (int i = 0; i < arraysize(queryResultBuffer); ++i)
{
success = device->CreateBuffer(&bd, nullptr, &queryResultBuffer[i]);
assert(success);
}
#if PERFORMANCEAPI_ENABLED
superluminal_handle = PerformanceAPI_LoadFrom(L"PerformanceAPI.dll", &superluminal_functions);
if (superluminal_handle)
{
wi::backlog::post("[wi::profiler] Superluminal Performance API loaded");
}
#endif // PERFORMANCEAPI_ENABLED
}
cpu_frame = BeginRangeCPU("CPU Frame");
GraphicsDevice* device = wi::graphics::GetDevice();
CommandList cmd = device->BeginCommandList();
queryheap_idx = device->GetBufferIndex();
// Read results of previous timings:
// This should be done before we begin reallocating new queries for current buffer index
const uint64_t* queryResults = (const uint64_t*)queryResultBuffer[queryheap_idx].mapped_data;
double gpu_frequency = (double)device->GetTimestampFrequency() / 1000.0;
for (auto& x : ranges)
{
auto& range = x.second;
if (!range.in_use)
continue;
if (!range.IsCPURange())
{
const int begin_idx = range.gpuBegin[queryheap_idx];
const int end_idx = range.gpuEnd[queryheap_idx];
if (queryResults != nullptr && begin_idx >= 0 && end_idx >= 0)
{
const uint64_t begin_result = queryResults[begin_idx];
const uint64_t end_result = queryResults[end_idx];
range.time = (float)abs((double)(end_result - begin_result) / gpu_frequency);
}
range.gpuBegin[queryheap_idx] = -1;
range.gpuEnd[queryheap_idx] = -1;
}
range.times[range.avg_counter++ % arraysize(range.times)] = range.time;
if (range.avg_counter > arraysize(range.times))
{
float avg_time = 0;
for (int i = 0; i < arraysize(range.times); ++i)
{
avg_time += range.times[i];
}
range.time = avg_time / arraysize(range.times);
}
range.in_use = false;
}
device->QueryReset(
&queryHeap,
0,
queryHeap.desc.query_count,
cmd
);
gpu_frame = BeginRangeGPU("GPU Frame", cmd);
drawn_this_frame = false;
}
void EndFrame(CommandList cmd)
{
if (!ENABLED || !initialized)
return;
GraphicsDevice* device = wi::graphics::GetDevice();
// note: read the GPU Frame end range manually because it will be on a separate command list than start point:
auto& gpu_range = ranges[gpu_frame];
gpu_range.gpuEnd[queryheap_idx] = nextQuery.fetch_add(1);
device->QueryEnd(&queryHeap, gpu_range.gpuEnd[queryheap_idx], cmd);
EndRange(cpu_frame);
device->QueryResolve(
&queryHeap,
0,
nextQuery.load(),
&queryResultBuffer[queryheap_idx],
0ull,
cmd
);
nextQuery.store(0);
}
range_id BeginRangeCPU(const char* name)
{
if (!ENABLED || !initialized)
return 0;
#if PERFORMANCEAPI_ENABLED
if (superluminal_handle)
{
superluminal_functions.BeginEvent(name, nullptr, 0xFF0000FF);
}
#endif // PERFORMANCEAPI_ENABLED
range_id id = wi::helper::string_hash(name);
lock.lock();
// If one range name is hit multiple times, differentiate between them!
size_t differentiator = 0;
while (ranges[id].in_use)
{
wi::helper::hash_combine(id, differentiator++);
}
ranges[id].in_use = true;
ranges[id].name = name;
ranges[id].cpuTimer.record();
lock.unlock();
return id;
}
range_id BeginRangeGPU(const char* name, CommandList cmd)
{
if (!ENABLED || !initialized)
return 0;
range_id id = wi::helper::string_hash(name);
lock.lock();
// If one range name is hit multiple times, differentiate between them!
size_t differentiator = 0;
while (ranges[id].in_use)
{
wi::helper::hash_combine(id, differentiator++);
}
ranges[id].in_use = true;
ranges[id].name = name;
ranges[id].cmd = cmd;
GraphicsDevice* device = wi::graphics::GetDevice();
ranges[id].gpuBegin[queryheap_idx] = nextQuery.fetch_add(1);
device->QueryEnd(&queryHeap, ranges[id].gpuBegin[queryheap_idx], cmd);
lock.unlock();
return id;
}
void EndRange(range_id id)
{
if (!ENABLED || !initialized)
return;
lock.lock();
auto it = ranges.find(id);
if (it != ranges.end())
{
if (it->second.IsCPURange())
{
it->second.time = (float)it->second.cpuTimer.elapsed();
#if PERFORMANCEAPI_ENABLED
if (superluminal_handle)
{
superluminal_functions.EndEvent();
}
#endif // PERFORMANCEAPI_ENABLED
}
else
{
GraphicsDevice* device = wi::graphics::GetDevice();
ranges[id].gpuEnd[queryheap_idx] = nextQuery.fetch_add(1);
device->QueryEnd(&queryHeap, it->second.gpuEnd[queryheap_idx], it->second.cmd);
}
}
else
{
assert(0);
}
lock.unlock();
}
PipelineState pso_linestrip;
PipelineState pso_linelist;
const uint32_t graph_vertex_count = 120;
float cpu_graph[graph_vertex_count] = {};
float gpu_graph[graph_vertex_count] = {};
float cpu_memory_graph[graph_vertex_count] = {};
float gpu_memory_graph[graph_vertex_count] = {};
void LoadShaders()
{
GraphicsDevice* device = wi::graphics::GetDevice();
PipelineStateDesc desc;
desc.vs = wi::renderer::GetShader(wi::enums::VSTYPE_VERTEXCOLOR);
desc.ps = wi::renderer::GetShader(wi::enums::PSTYPE_VERTEXCOLOR);
desc.il = wi::renderer::GetInputLayout(wi::enums::ILTYPE_VERTEXCOLOR);
desc.dss = wi::renderer::GetDepthStencilState(wi::enums::DSSTYPE_DEPTHDISABLED);
desc.rs = wi::renderer::GetRasterizerState(wi::enums::RSTYPE_WIRE_SMOOTH);
desc.bs = wi::renderer::GetBlendState(wi::enums::BSTYPE_TRANSPARENT);
desc.pt = PrimitiveTopology::LINESTRIP;
bool success = device->CreatePipelineState(&desc, &pso_linestrip);
desc.pt = PrimitiveTopology::LINELIST;
success = device->CreatePipelineState(&desc, &pso_linelist);
assert(success);
}
struct Hits
{
uint32_t num_hits = 0;
float total_time = 0;
};
wi::unordered_map<std::string, Hits> time_cache_cpu;
wi::unordered_map<std::string, Hits> time_cache_gpu;
void DrawData(
const wi::Canvas& canvas,
float x,
float y,
CommandList cmd,
ColorSpace colorspace
)
{
if (!ENABLED || !ENABLED_REQUEST || !initialized || drawn_this_frame)
return;
drawn_this_frame = true;
const XMFLOAT2 graph_size = XMFLOAT2(190, 100);
const float graph_left_offset = 45;
const float graph_padding_y = 40;
wi::image::SetCanvas(canvas);
wi::font::SetCanvas(canvas);
std::stringstream ss("");
ss.precision(2);
for (auto& x : ranges)
{
if (!x.second.in_use)
continue;
if (x.second.IsCPURange())
{
if (x.first == cpu_frame)
continue;
time_cache_cpu[x.second.name].num_hits++;
time_cache_cpu[x.second.name].total_time += x.second.time;
}
else
{
if (x.first == gpu_frame)
continue;
time_cache_gpu[x.second.name].num_hits++;
time_cache_gpu[x.second.name].total_time += x.second.time;
}
}
// Print CPU ranges:
ss << ranges[cpu_frame].name << ": " << std::fixed << ranges[cpu_frame].time << " ms" << std::endl;
for (auto& x : time_cache_cpu)
{
if (x.second.num_hits > 1)
{
ss << "\t" << x.first << " (" << x.second.num_hits << "x)" << ": " << std::fixed << x.second.total_time << " ms" << std::endl;
}
else if(x.second.num_hits == 1)
{
ss << "\t" << x.first << ": " << std::fixed << x.second.total_time << " ms" << std::endl;
}
x.second.num_hits = 0;
x.second.total_time = 0;
}
ss << std::endl;
// Print GPU ranges:
ss << ranges[gpu_frame].name << ": " << std::fixed << ranges[gpu_frame].time << " ms" << std::endl;
for (auto& x : time_cache_gpu)
{
if (x.second.num_hits > 1)
{
ss << "\t" << x.first << " (" << x.second.num_hits << "x)" << ": " << std::fixed << x.second.total_time << " ms" << std::endl;
}
else if (x.second.num_hits == 1)
{
ss << "\t" << x.first << ": " << std::fixed << x.second.total_time << " ms" << std::endl;
}
x.second.num_hits = 0;
x.second.total_time = 0;
}
wi::font::Params params = wi::font::Params(x, y + (graph_size.y + graph_padding_y) * 2, wi::font::WIFONTSIZE_DEFAULT - 6, wi::font::WIFALIGN_LEFT, wi::font::WIFALIGN_TOP, text_color);
// Background:
wi::image::Params fx;
fx.pos.x = x - 10;
fx.pos.y = y - 10;
fx.siz.x = std::max(graph_size.x, (float)wi::font::TextWidth(ss.str(), params)) + 200;
fx.siz.y = (float)wi::font::TextHeight(ss.str(), params) + (graph_size.y + graph_padding_y) * 2 + 20;
fx.color = background_color;
if (colorspace != ColorSpace::SRGB)
{
params.enableLinearOutputMapping(9);
fx.enableLinearOutputMapping(9);
}
wi::image::Draw(nullptr, fx, cmd);
wi::font::Draw(ss.str(), params, cmd);
// Graph:
{
GraphicsDevice* device = wi::graphics::GetDevice();
wi::graphics::GraphicsDevice::MemoryUsage gpu_memory_usage = device->GetMemoryUsage();
wi::helper::MemoryUsage cpu_memory_usage = wi::helper::GetMemoryUsage();
static bool shaders_loaded = false;
if (!shaders_loaded)
{
shaders_loaded = true;
static wi::eventhandler::Handle handle = wi::eventhandler::Subscribe(wi::eventhandler::EVENT_RELOAD_SHADERS, [](uint64_t userdata) { LoadShaders(); });
LoadShaders();
}
float graph_max = 33;
float graph_max_gpu_memory = 0;
float graph_max_cpu_memory = 0;
for (uint32_t i = graph_vertex_count - 1; i > 0; --i)
{
cpu_graph[i] = cpu_graph[i - 1];
gpu_graph[i] = gpu_graph[i - 1];
cpu_memory_graph[i] = cpu_memory_graph[i - 1];
gpu_memory_graph[i] = gpu_memory_graph[i - 1];
graph_max = std::max(graph_max, cpu_graph[i]);
graph_max = std::max(graph_max, gpu_graph[i]);
graph_max_gpu_memory = std::max(graph_max_gpu_memory, gpu_memory_graph[i]);
graph_max_cpu_memory = std::max(graph_max_cpu_memory, cpu_memory_graph[i]);
}
cpu_graph[0] = ranges[cpu_frame].time;
gpu_graph[0] = ranges[gpu_frame].time;
cpu_memory_graph[0] = float(double(cpu_memory_usage.process_physical) / (1024.0 * 1024.0 * 1024.0)); // Gigabytes
gpu_memory_graph[0] = float(double(gpu_memory_usage.usage) / (1024.0 * 1024.0 * 1024.0)); // Gigabytes
graph_max = std::max(graph_max, cpu_graph[0]);
graph_max = std::max(graph_max, gpu_graph[0]);
graph_max_gpu_memory = std::max(graph_max_gpu_memory, gpu_memory_graph[0]);
graph_max_cpu_memory = std::max(graph_max_cpu_memory, cpu_memory_graph[0]);
const float graph_max_memory = std::max(graph_max_cpu_memory, graph_max_gpu_memory) * 1.1f;
struct Vertex
{
XMFLOAT4 position;
XMFLOAT4 color;
};
const Vertex graph_info[] = {
// axes:
{XMFLOAT4(0, 0, 0, 1), text_color},
{XMFLOAT4(0, 1, 0, 1), text_color},
{XMFLOAT4(0, 1, 0, 1), text_color},
{XMFLOAT4(1, 1, 0, 1), text_color},
// markers:
{XMFLOAT4(0, 0, 0, 1), text_color}, // graph_max
{XMFLOAT4(-10.0f / graph_size.x, 0, 0, 1), text_color}, // graph_max
{XMFLOAT4(0, 1 - 16.6f / graph_max, 0, 1), text_color}, // 16.6f
{XMFLOAT4(-10.0f / graph_size.x, 1 - 16.6f / graph_max, 0, 1), text_color}, // 16.6f
{XMFLOAT4(60.0f / float(graph_vertex_count),1,0,1), text_color}, // 60 frames
{XMFLOAT4(60.0f / float(graph_vertex_count),1 + 10.0f / graph_size.x,0,1), text_color}, // 60 frames
{XMFLOAT4(1,1,0,1), text_color}, // 0 frames
{XMFLOAT4(1,1 + 10.0f / graph_size.x,0,1), text_color}, // 0 frames
};
const Vertex graph_memory_info[] = {
// axes:
{XMFLOAT4(0, 0, 0, 1), text_color},
{XMFLOAT4(0, 1, 0, 1), text_color},
{XMFLOAT4(0, 1, 0, 1), text_color},
{XMFLOAT4(1, 1, 0, 1), text_color},
// markers:
{XMFLOAT4(0, 0, 0, 1), text_color}, // graph_max_memory
{XMFLOAT4(-10.0f / graph_size.x, 0, 0, 1), text_color}, // graph_max_memory
{XMFLOAT4(0, 0.5f, 0, 1), text_color}, // half_memory_budget
{XMFLOAT4(-10.0f / graph_size.x, 0.5f, 0, 1), text_color}, // half_memory_budget
{XMFLOAT4(60.0f / float(graph_vertex_count),1,0,1), text_color}, // 60 frames
{XMFLOAT4(60.0f / float(graph_vertex_count),1 + 10.0f / graph_size.x,0,1), text_color}, // 60 frames
{XMFLOAT4(1,1,0,1), text_color}, // 0 frames
{XMFLOAT4(1,1 + 10.0f / graph_size.x,0,1), text_color}, // 0 frames
};
GraphicsDevice::GPUAllocation allocation = device->AllocateGPU(sizeof(Vertex) * graph_vertex_count * 4 + sizeof(graph_info) + sizeof(graph_memory_info), cmd);
for (uint32_t i = 0; i < graph_vertex_count; ++i)
{
Vertex vert;
vert.color = XMFLOAT4(1, 0.2f, 0.2f, 1);
vert.position = XMFLOAT4(float(graph_vertex_count - 1 - i) / float(graph_vertex_count), 1 - cpu_graph[i] / graph_max, 0, 1);
std::memcpy((Vertex*)allocation.data + i, &vert, sizeof(vert));
vert.color = XMFLOAT4(0.2f, 1, 0.2f, 1);
vert.position = XMFLOAT4(float(graph_vertex_count - 1 - i) / float(graph_vertex_count), 1 - gpu_graph[i] / graph_max, 0, 1);
std::memcpy((Vertex*)allocation.data + graph_vertex_count + i, &vert, sizeof(vert));
vert.color = XMFLOAT4(1, 1, 0.2f, 1);
vert.position = XMFLOAT4(float(graph_vertex_count - 1 - i) / float(graph_vertex_count), 1 - gpu_memory_graph[i] / graph_max_memory, 0, 1);
std::memcpy((Vertex*)allocation.data + graph_vertex_count * 2 + i, &vert, sizeof(vert));
vert.color = XMFLOAT4(1, 0.2f, 1, 1);
vert.position = XMFLOAT4(float(graph_vertex_count - 1 - i) / float(graph_vertex_count), 1 - cpu_memory_graph[i] / graph_max_memory, 0, 1);
std::memcpy((Vertex*)allocation.data + graph_vertex_count * 3 + i, &vert, sizeof(vert));
}
std::memcpy((Vertex*)allocation.data + graph_vertex_count * 4, graph_info, sizeof(graph_info));
std::memcpy((Vertex*)allocation.data + graph_vertex_count * 4 + sizeof(graph_info) / sizeof(Vertex), graph_memory_info, sizeof(graph_memory_info));
device->BindPipelineState(&pso_linestrip, cmd);
MiscCB cb;
cb.g_xColor = XMFLOAT4(1, 1, 1, 1);
XMStoreFloat4x4(&cb.g_xTransform,
XMMatrixScaling(graph_size.x - graph_left_offset, graph_size.y, 1) *
XMMatrixTranslation(x + graph_left_offset, y, 0) *
canvas.GetProjection()
);
device->BindDynamicConstantBuffer(cb, CB_GETBINDSLOT(MiscCB), cmd);
const GPUBuffer* buffers[] = {
&allocation.buffer
};
const uint32_t strides[] = {
sizeof(Vertex)
};
const uint64_t offsets[] = {
allocation.offset
};
device->BindVertexBuffers(buffers, 0, arraysize(buffers), strides, offsets, cmd);
device->Draw(graph_vertex_count, 0, cmd);
device->Draw(graph_vertex_count, graph_vertex_count, cmd);
device->BindPipelineState(&pso_linelist, cmd);
device->Draw(sizeof(graph_info) / sizeof(Vertex), graph_vertex_count * 4, cmd);
// Memory graph:
const float memory_graph_y = y + graph_size.y + graph_padding_y;
XMStoreFloat4x4(&cb.g_xTransform,
XMMatrixScaling(graph_size.x - graph_left_offset, graph_size.y, 1) *
XMMatrixTranslation(x + graph_left_offset, memory_graph_y, 0) *
canvas.GetProjection()
);
device->BindDynamicConstantBuffer(cb, CB_GETBINDSLOT(MiscCB), cmd);
device->BindPipelineState(&pso_linestrip, cmd);
device->Draw(graph_vertex_count, graph_vertex_count * 2, cmd);
device->Draw(graph_vertex_count, graph_vertex_count * 3, cmd);
device->BindPipelineState(&pso_linelist, cmd);
device->Draw(sizeof(graph_memory_info) / sizeof(Vertex), graph_vertex_count * 4 + sizeof(graph_info) / sizeof(Vertex), cmd);
wi::font::Params params;
params.size = 10;
params.v_align = wi::font::WIFALIGN_CENTER;
params.color = text_color;
params.position.x = x;
params.position.y = y;
std::stringstream ss;
ss.precision(1);
ss << std::fixed << graph_max << " ms";
wi::font::Draw(ss.str(), params, cmd);
params.position.y = y + graph_size.y - (16.6f / graph_max) * graph_size.y;
wi::font::Draw("16.6 ms", params, cmd);
params.position.x = x + graph_left_offset - 40;
params.position.y = memory_graph_y;
ss.str("");
ss << graph_max_memory << " GB";
wi::font::Draw(ss.str(), params, cmd);
params.position.y = memory_graph_y + graph_size.y - 0.5f * graph_size.y;
ss.str("");
ss << graph_max_memory * 0.5f << " GB";
wi::font::Draw(ss.str(), params, cmd);
params.position.x = x + graph_size.x + 5;
params.position.y = y + graph_size.y - cpu_graph[0] / graph_max * graph_size.y;
params.color = wi::Color::fromFloat4(XMFLOAT4(1, 0.2f, 0.2f, 1));
ss.str("");
ss.clear();
ss << "cpu: " << cpu_graph[0] << " ms";
wi::font::Draw(ss.str(), params, cmd);
float cpu_position = params.position.y;
params.position.x = x + graph_size.x + 5;
params.position.y = y + graph_size.y - gpu_graph[0] / graph_max * graph_size.y;
if (std::abs(params.position.y - cpu_position) < params.size)
{
if (params.position.y < cpu_position)
{
params.position.y = cpu_position - params.size;
}
else
{
params.position.y = cpu_position + params.size;
}
}
params.color = wi::Color::fromFloat4(XMFLOAT4(0.2f, 1, 0.2f, 1));
ss.str("");
ss.clear();
ss << "gpu: " << gpu_graph[0] << " ms";
wi::font::Draw(ss.str(), params, cmd);
params.position.x = x + graph_left_offset + graph_size.x - graph_left_offset + 5;
params.position.y = memory_graph_y + graph_size.y - cpu_memory_graph[0] / graph_max_memory * graph_size.y;
params.color = wi::Color::fromFloat4(XMFLOAT4(1, 0.2f, 1, 1));
ss.str("");
ss.clear();
ss << "RAM: " << cpu_memory_graph[0] << " GB";
wi::font::Draw(ss.str(), params, cmd);
cpu_position = params.position.y;
params.position.y = memory_graph_y + graph_size.y - gpu_memory_graph[0] / graph_max_memory * graph_size.y;
if (std::abs(params.position.y - cpu_position) < params.size)
{
if (params.position.y < cpu_position)
{
params.position.y = cpu_position - params.size;
}
else
{
params.position.y = cpu_position + params.size;
}
}
params.color = wi::Color::fromFloat4(XMFLOAT4(1, 1, 0.2f, 1));
ss.str("");
ss.clear();
ss << "VRAM: " << gpu_memory_graph[0] << " GB";
wi::font::Draw(ss.str(), params, cmd);
params.h_align = wi::font::WIFALIGN_CENTER;
params.color = text_color;
params.position.x = x + graph_left_offset + (graph_size.x - graph_left_offset) * 0.5f;
params.position.y = y + graph_size.y + 10;
wi::font::Draw("60 frames", params, cmd);
params.position.x = x + graph_size.x;
wi::font::Draw("current frame", params, cmd);
// Memory graph:
params.position.x = x + graph_left_offset + (graph_size.x - graph_left_offset) * 0.5f;
params.position.y = memory_graph_y + graph_size.y + 10;
wi::font::Draw("60 frames", params, cmd);
params.position.x = x + graph_left_offset + graph_size.x - graph_left_offset;
wi::font::Draw("current frame", params, cmd);
}
}
void DisableDrawForThisFrame()
{
drawn_this_frame = true;
}
void SetEnabled(bool value)
{
// Don't enable/disable the profiler immediately, only on the next frame
// to avoid enabling inside a Begin/End by mistake
ENABLED_REQUEST = value;
}
bool IsEnabled()
{
return ENABLED;
}
void SetBackgroundColor(wi::Color color)
{
background_color = color;
}
void SetTextColor(wi::Color color)
{
text_color = color;
}
}