| e8555c89 Andy McCall |
1 |
#include "ImGuiTraceViewer.hh" |
| 2 |
|
| 3 |
#include "ImGuiCpp.hh" |
| 4 |
#include "ImGuiOpenFile.hh" |
| 5 |
#include "ImGuiUtils.hh" |
| 6 |
#include "Shortcuts.hh" |
| 7 |
|
| 8 |
#include "Debugger.hh" |
| 9 |
#include "EmuDuration.hh" |
| 10 |
#include "MSXMotherBoard.hh" |
| 11 |
#include "ReverseManager.hh" |
| 12 |
|
| 13 |
#include "find_closest.h" |
| 14 |
#include "one_of.hh" |
| 15 |
#include "timeline.hh" |
| 16 |
|
| 17 |
#include "imgui_stdlib.h" |
| 18 |
|
| 19 |
#include <algorithm> |
| 20 |
#include <cassert> |
| 21 |
|
| 22 |
namespace openmsx { |
| 23 |
|
| 24 |
using Traces = std::span<Tracer::Trace*>; |
| 25 |
using Events = std::span<const Tracer::Event>; |
| 26 |
|
| 27 |
constexpr float splitterWidth = 7.0f; |
| 28 |
|
| 29 |
struct Convertor { |
| 30 |
EmuTime viewStartTime; |
| 31 |
float viewScreenWidth; |
| 32 |
float pixelsPerTick; |
| 33 |
|
| 34 |
Convertor(EmuTime viewStartTime_, EmuDuration viewDuration, float viewScreenWidth_) |
| 35 |
: viewStartTime(viewStartTime_) |
| 36 |
, viewScreenWidth(viewScreenWidth_) |
| 37 |
, pixelsPerTick(viewScreenWidth_ / float(viewDuration.toUint64())) |
| 38 |
{} |
| 39 |
|
| 40 |
[[nodiscard]] float timeToX(EmuTime time) const { |
| 41 |
int64_t dt = int64_t(time.toUint64()) - int64_t(viewStartTime.toUint64()); |
| 42 |
return float(dt) * pixelsPerTick; |
| 43 |
} |
| 44 |
|
| 45 |
[[nodiscard]] EmuDuration deltaXtoDuration(float deltaX) const { |
| 46 |
return EmuDuration(uint64_t(deltaX / pixelsPerTick)); |
| 47 |
} |
| 48 |
[[nodiscard]] EmuTime xToTime(float x) const { |
| 49 |
return viewStartTime + deltaXtoDuration(x); |
| 50 |
} |
| 51 |
}; |
| 52 |
|
| 53 |
static auto calcRulerTicks(double begin, double end, double n) |
| 54 |
{ |
| 55 |
struct Result { |
| 56 |
double first; |
| 57 |
double step; |
| 58 |
}; |
| 59 |
|
| 60 |
auto length = end - begin; |
| 61 |
auto i = length / n; |
| 62 |
auto magnitude = std::pow(10.0, std::floor(std::log10(i))); |
| 63 |
auto mantissa = i / magnitude; |
| 64 |
auto snap = (mantissa >= 5.0) ? 10.0 |
| 65 |
: (mantissa >= 2.0) ? 5.0 |
| 66 |
: 2.0; |
| 67 |
auto step = snap * magnitude; |
| 68 |
auto first = std::floor(begin / step) * step; |
| 69 |
return Result{.first = first, .step = step}; |
| 70 |
} |
| 71 |
|
| 72 |
|
| 73 |
|
| 74 |
|
| 75 |
|
| 76 |
|
| 77 |
|
| 78 |
|
| 79 |
|
| 80 |
|
| 81 |
|
| 82 |
|
| 83 |
std::pair<const std::vector<TimelineFormatter::Label>&, double> |
| 84 |
TimelineFormatter::calc(double from, double to, double numLabels, double unitFactor) |
| 85 |
{ |
| 86 |
|
| 87 |
Params params{.from = from, .to = to, .numLabels = numLabels, .unitFactor = unitFactor}; |
| 88 |
if (params == prevParams) return {labels, step}; |
| 89 |
prevParams = params; |
| 90 |
|
| 91 |
auto ruler = calcRulerTicks(from * unitFactor, to * unitFactor, numLabels); |
| 92 |
auto start = ruler.first; |
| 93 |
step = ruler.step; |
| 94 |
auto stop = start + step * std::ceil(numLabels); |
| 95 |
|
| 96 |
|
| 97 |
|
| 98 |
|
| 99 |
auto testValue = (start == 0.0) ? stop : start; |
| 100 |
auto absTestValue = std::abs(testValue); |
| 101 |
|
| 102 |
|
| 103 |
auto logStep = std::log10(step); |
| 104 |
int ceilNegLogStep = -static_cast<int>(std::floor( |
| 105 |
logStep)); |
| 106 |
int e = static_cast<int>(std::floor(std::log10(absTestValue))); |
| 107 |
|
| 108 |
|
| 109 |
|
| 110 |
|
| 111 |
|
| 112 |
|
| 113 |
int sciPrecision = e + ceilNegLogStep; |
| 114 |
int sciLen = sciPrecision + 5; |
| 115 |
|
| 116 |
|
| 117 |
|
| 118 |
int decimals = (step < 1.0) ? ceilNegLogStep : 0; |
| 119 |
int intDigits = |
| 120 |
(absTestValue < 1.0) |
| 121 |
? 1 |
| 122 |
: e + 1; |
| 123 |
int fixedLen = intDigits + decimals; |
| 124 |
|
| 125 |
|
| 126 |
|
| 127 |
|
| 128 |
|
| 129 |
bool useScientific = sciLen < fixedLen; |
| 130 |
|
| 131 |
|
| 132 |
|
| 133 |
|
| 134 |
labels.clear(); |
| 135 |
auto value = start; |
| 136 |
char buf[32]; |
| 137 |
if (useScientific) { |
| 138 |
while (value < stop) { |
| 139 |
int len = snprintf(buf, sizeof(buf), "%.*e", |
| 140 |
sciPrecision, value); |
| 141 |
labels.push_back( |
| 142 |
{value / unitFactor, std::string(buf, len)}); |
| 143 |
value += step; |
| 144 |
} |
| 145 |
} else { |
| 146 |
while (value < stop) { |
| 147 |
int len = snprintf(buf, sizeof(buf), "%.*f", |
| 148 |
decimals, value); |
| 149 |
labels.push_back( |
| 150 |
{value / unitFactor, std::string(buf, len)}); |
| 151 |
value += step; |
| 152 |
} |
| 153 |
} |
| 154 |
return {labels, step}; |
| 155 |
} |
| 156 |
|
| 157 |
ImGuiTraceViewer::ImGuiTraceViewer(ImGuiManager& manager_) |
| 158 |
: ImGuiPart(manager_) |
| 159 |
{ |
| 160 |
} |
| 161 |
|
| 162 |
void ImGuiTraceViewer::save(ImGuiTextBuffer& buf) |
| 163 |
{ |
| 164 |
savePersistent(buf, *this, persistentElements); |
| 165 |
} |
| 166 |
|
| 167 |
void ImGuiTraceViewer::loadLine(std::string_view name, zstring_view value) |
| 168 |
{ |
| 169 |
loadOnePersistent(name, value, *this, persistentElements); |
| 170 |
} |
| 171 |
|
| 172 |
static void drawText(ImDrawList* drawList, gl::vec2 pos, ImU32 color, std::string_view text) |
| 173 |
{ |
| 174 |
drawList->AddText(pos, color, text.data(), text.data() + text.size()); |
| 175 |
} |
| 176 |
|
| 177 |
struct DrawCoarse { |
| 178 |
const Convertor* convertor; |
| 179 |
ImDrawList* drawList; |
| 180 |
float x; |
| 181 |
float y0, y1; |
| 182 |
ImU32 colorNormal; |
| 183 |
ImU32 colorHover; |
| 184 |
Tracer::Trace::Format format; |
| 185 |
bool rowHovered; |
| 186 |
|
| 187 |
void operator()(float x0, float x1, auto first, auto last) const { |
| 188 |
auto xf0 = std::floor(x0 + x); |
| 189 |
auto xf1 = std::floor(x1 + x + 1.0f); |
| 190 |
drawList->AddRectFilled({xf0, y0}, {xf1, y1}, colorNormal); |
| 191 |
|
| 192 |
if (!rowHovered) return; |
| 193 |
auto mouseX = ImGui::GetIO().MousePos.x; |
| 194 |
if (mouseX < xf0 || xf1 <= mouseX) return; |
| 195 |
drawList->AddLine({mouseX, y0}, {mouseX, y1}, colorHover, 2.0f); |
| 196 |
|
| 197 |
if (!convertor) return; |
| 198 |
auto [it, _] = find_closest( |
| 199 |
first, last, convertor->xToTime(mouseX), {}, &Tracer::Event::time); |
| 200 |
if (it == last) return; |
| 201 |
if (it->value.template holds_alternative<std::monostate>()) { |
| 202 |
++it; |
| 203 |
if (it == last) return; |
| 204 |
} |
| 205 |
std::array<char, 64> tmpBuf; |
| 206 |
auto valueStr = ImGuiTraceViewer::formatTraceValue(it->value, tmpBuf, format); |
| 207 |
im::Tooltip([&]{ |
| 208 |
ImGui::TextUnformatted(valueStr); |
| 209 |
}); |
| 210 |
} |
| 211 |
}; |
| 212 |
|
| 213 |
static void drawEventsVoid( |
| 214 |
gl::vec2 topLeft, const Convertor& convertor, EmuTime maxT, Events events, bool rowHovered) |
| 215 |
{ |
| 216 |
auto* drawList = ImGui::GetWindowDrawList(); |
| 217 |
auto colorLine = ImGui::GetColorU32(ImGuiCol_PlotLines); |
| 218 |
auto h = ImGui::GetFrameHeight(); |
| 219 |
auto h5 = h * 0.20f; |
| 220 |
auto y0 = topLeft.y; |
| 221 |
auto y1 = y0 + h; |
| 222 |
auto y5 = y0 + h5; |
| 223 |
|
| 224 |
auto minX = drawList->GetClipRectMin().x; |
| 225 |
auto maxX = drawList->GetClipRectMax().x; |
| 226 |
drawList->AddLine({minX, y1}, {maxX, y1}, colorLine); |
| 227 |
if (events.empty()) return; |
| 228 |
|
| 229 |
auto highLightTime = [&] { |
| 230 |
if (!rowHovered) return EmuTime::infinity(); |
| 231 |
const auto& io = ImGui::GetIO(); |
| 232 |
auto mouseX = io.MousePos.x - topLeft.x; |
| 233 |
auto [it, dist] = find_closest( |
| 234 |
events, convertor.xToTime(mouseX), {}, &Tracer::Event::time); |
| 235 |
assert(it != events.end()); |
| 236 |
return (dist < convertor.deltaXtoDuration(h5)) ? it->time : EmuTime::infinity(); |
| 237 |
}(); |
| 238 |
|
| 239 |
auto colorNormal = ImGui::GetColorU32(ImGuiCol_PlotHistogram); |
| 240 |
auto colorHover = ImGui::GetColorU32(ImGuiCol_PlotHistogramHovered); |
| 241 |
|
| 242 |
auto drawDetailed = [&](float x0, float , const Tracer::Event& event) { |
| 243 |
x0 += topLeft.x; |
| 244 |
std::array<ImVec2, 3> points = { |
| 245 |
gl::vec2{x0 - h5, y5}, |
| 246 |
gl::vec2{x0 , y0}, |
| 247 |
gl::vec2{x0 + h5, y5}, |
| 248 |
}; |
| 249 |
bool hover = event.time == highLightTime; |
| 250 |
auto color = hover ? colorHover : colorNormal; |
| 251 |
auto thickness = hover ? 2.0f : 1.0f; |
| 252 |
drawList->AddPolyline(points.data(), narrow<int>(points.size()), color, thickness); |
| 253 |
drawList->AddLine({x0, y0}, {x0, y1}, color, thickness); |
| 254 |
}; |
| 255 |
auto dummyFormat = Tracer::Trace::Format::DEC; |
| 256 |
processTimeline(events, maxT, convertor, drawDetailed, |
| 257 |
DrawCoarse{nullptr, drawList, topLeft.x, y0, y1, colorNormal, colorHover, dummyFormat, rowHovered}); |
| 258 |
} |
| 259 |
|
| 260 |
static void drawEventsBool( |
| 261 |
gl::vec2 topLeft, const Convertor& convertor, EmuTime maxT, Events events, bool rowHovered) |
| 262 |
{ |
| 263 |
if (events.empty()) return; |
| 264 |
|
| 265 |
auto* drawList = ImGui::GetWindowDrawList(); |
| 266 |
auto colorNormal = ImGui::GetColorU32(ImGuiCol_PlotLines); |
| 267 |
auto colorHover = ImGui::GetColorU32(ImGuiCol_PlotLinesHovered); |
| 268 |
auto h = ImGui::GetFrameHeight(); |
| 269 |
auto y0 = topLeft.y; |
| 270 |
auto y1 = y0 + h; |
| 271 |
|
| 272 |
const auto& io = ImGui::GetIO(); |
| 273 |
auto mouseX = io.MousePos.x; |
| 274 |
|
| 275 |
auto getVal = [&](const Tracer::Event& e) { |
| 276 |
return e.value.get_as_bool() ? y0 : y1; |
| 277 |
}; |
| 278 |
|
| 279 |
auto yp = y1; |
| 280 |
auto drawDetailed = [&](float x0, float x1, const Tracer::Event& event) { |
| 281 |
auto xf0 = topLeft.x + x0; |
| 282 |
auto xf1 = topLeft.x + x1; |
| 283 |
auto y = getVal(event); |
| 284 |
|
| 285 |
std::array<ImVec2, 3> points = { |
| 286 |
gl::vec2{xf1, y}, |
| 287 |
gl::vec2{xf0, y}, |
| 288 |
gl::vec2{xf0, yp}, |
| 289 |
}; |
| 290 |
auto hover = rowHovered && xf0 <= mouseX && mouseX < xf1; |
| 291 |
auto color = hover ? colorHover : colorNormal; |
| 292 |
auto thickness = hover ? 2.0f : 1.0f; |
| 293 |
drawList->AddPolyline(points.data(), (yp == y) ? 2 : 3, color, thickness); |
| 294 |
|
| 295 |
yp = y; |
| 296 |
}; |
| 297 |
auto dummyFormat = Tracer::Trace::Format::DEC; |
| 298 |
processTimeline(events, maxT, convertor, drawDetailed, |
| 299 |
DrawCoarse{nullptr, drawList, topLeft.x, y0, y1, colorNormal, colorHover, dummyFormat, rowHovered}); |
| 300 |
} |
| 301 |
|
| 302 |
[[nodiscard]] std::string_view ImGuiTraceViewer::formatTraceValue(const TraceValue& value, std::span<char, 64> tmpBuf, |
| 303 |
Tracer::Trace::Format format) |
| 304 |
{ |
| 305 |
return value.visit(overloaded{ |
| 306 |
[](std::monostate) { |
| 307 |
return std::string_view{}; |
| 308 |
}, |
| 309 |
[&](uint64_t i) { |
| 310 |
switch (format) { |
| 311 |
using enum Tracer::Trace::Format; |
| 312 |
case BIN: { |
| 313 |
auto bin = bin_string(i); |
| 314 |
auto* begin = tmpBuf.data(); |
| 315 |
auto* end = bin.copy(begin); |
| 316 |
return std::string_view(begin, end); |
| 317 |
} |
| 318 |
default: |
| 319 |
case DEC: { |
| 320 |
auto [end, ec] = std::to_chars(tmpBuf.data(), tmpBuf.data() + tmpBuf.size(), i); |
| 321 |
assert(ec == std::errc{}); |
| 322 |
return std::string_view(tmpBuf.data(), end - tmpBuf.data()); |
| 323 |
} |
| 324 |
case HEX: { |
| 325 |
auto hex = hex_string(i); |
| 326 |
auto* begin = tmpBuf.data(); |
| 327 |
auto* end = hex.copy(begin); |
| 328 |
return std::string_view(begin, end); |
| 329 |
} |
| 330 |
} |
| 331 |
}, |
| 332 |
[&](double d) { |
| 333 |
#if 0 |
| 334 |
|
| 335 |
|
| 336 |
|
| 337 |
|
| 338 |
|
| 339 |
#endif |
| 340 |
int len = snprintf(tmpBuf.data(), tmpBuf.size(), "%.17g", d); |
| 341 |
assert(len > 0 && size_t(len) < tmpBuf.size()); |
| 342 |
return std::string_view(tmpBuf.data(), len); |
| 343 |
}, |
| 344 |
[](std::string_view s) { |
| 345 |
return s; |
| 346 |
} |
| 347 |
}); |
| 348 |
} |
| 349 |
|
| 350 |
static void drawEventsValue( |
| 351 |
gl::vec2 topLeft, const Convertor& convertor, EmuTime maxT, Events events, bool rowHovered, |
| 352 |
Tracer::Trace::Format format) |
| 353 |
{ |
| 354 |
auto* drawList = ImGui::GetWindowDrawList(); |
| 355 |
const auto& style = ImGui::GetStyle(); |
| 356 |
auto colorNormal = ImGui::GetColorU32(ImGuiCol_PlotLines); |
| 357 |
auto colorHover = ImGui::GetColorU32(ImGuiCol_PlotLinesHovered); |
| 358 |
auto colorText = getColor(imColor::TEXT); |
| 359 |
auto h = ImGui::GetFrameHeight(); |
| 360 |
auto h2 = h * 0.50f; |
| 361 |
auto h5 = h * 0.20f; |
| 362 |
auto y0 = topLeft.y; |
| 363 |
auto y1 = y0 + h; |
| 364 |
auto y2 = y0 + h2; |
| 365 |
|
| 366 |
const auto& io = ImGui::GetIO(); |
| 367 |
auto mouseX = io.MousePos.x; |
| 368 |
|
| 369 |
auto drawDetailed = [&](float x0, float x1, const Tracer::Event& event) { |
| 370 |
auto xf0 = topLeft.x + x0; |
| 371 |
auto xf1 = topLeft.x + x1; |
| 372 |
auto hover = rowHovered && xf0 <= mouseX && mouseX < xf1; |
| 373 |
auto color = hover ? colorHover : colorNormal; |
| 374 |
auto thickness = hover ? 2.0f : 1.0f; |
| 375 |
|
| 376 |
auto tl = gl::vec2{xf0, y0}; |
| 377 |
auto br = gl::vec2{xf1, y1}; |
| 378 |
if (event.value.holds_alternative<std::monostate>()) { |
| 379 |
drawList->AddLine({xf0, y2}, {xf1, y2}, color, thickness); |
| 380 |
} else { |
| 381 |
std::array<char, 64> tmpBuf; |
| 382 |
auto valueStr = ImGuiTraceViewer::formatTraceValue(event.value, tmpBuf, format); |
| 383 |
if (auto maxWidth = x1 - x0 - 2.0f * h5; maxWidth > 0.0f) { |
| 384 |
std::array<ImVec2, 6> points = { |
| 385 |
gl::vec2{xf1 - h5, y0}, |
| 386 |
gl::vec2{xf0 + h5, y0}, |
| 387 |
gl::vec2{xf0, y2}, |
| 388 |
gl::vec2{xf0 + h5, y1}, |
| 389 |
gl::vec2{xf1 - h5, y1}, |
| 390 |
gl::vec2{xf1, y2}, |
| 391 |
}; |
| 392 |
drawList->AddPolyline(points.data(), 6, color, thickness, ImDrawFlags_Closed); |
| 393 |
drawList->PushClipRect(tl, br, true); |
| 394 |
auto textX = std::max(xf0 + h5, topLeft.x); |
| 395 |
drawText(drawList, gl::vec2{textX, y0 + style.FramePadding.y}, colorText, valueStr); |
| 396 |
drawList->PopClipRect(); |
| 397 |
} else { |
| 398 |
drawList->AddRect(tl, br, color, {}, thickness); |
| 399 |
} |
| 400 |
if (hover) { |
| 401 |
im::Tooltip([&]{ |
| 402 |
ImGui::TextUnformatted(valueStr); |
| 403 |
}); |
| 404 |
} |
| 405 |
} |
| 406 |
}; |
| 407 |
processTimeline(events, maxT, convertor, drawDetailed, |
| 408 |
DrawCoarse{&convertor, drawList, topLeft.x, y0, y1, colorNormal, colorHover, format, rowHovered}); |
| 409 |
} |
| 410 |
|
| 411 |
static bool menuItemWithShortcut(ImGuiManager& manager, const char* label, Shortcuts::ID id, bool* p_selected = nullptr) |
| 412 |
{ |
| 413 |
const auto& shortcuts = manager.getShortcuts(); |
| 414 |
const auto& shortcut = shortcuts.getShortcut(id); |
| 415 |
if (shortcut.keyChord == ImGuiKey_None) { |
| 416 |
return ImGui::MenuItem(label, nullptr, p_selected); |
| 417 |
} else { |
| 418 |
return ImGui::MenuItem(label, getKeyChordName(shortcut.keyChord).c_str(), p_selected); |
| 419 |
} |
| 420 |
} |
| 421 |
|
| 422 |
void ImGuiTraceViewer::drawMenuBar(EmuTime minT, EmuDuration totalT, Tracer& tracer) |
| 423 |
{ |
| 424 |
im::Menu("File", [&]{ |
| 425 |
ImGui::MenuItem("Select probes and traces", nullptr, &showSelect); |
| 426 |
if (ImGui::MenuItem("Export as VCD ...")) { |
| 427 |
manager.openFile->selectNewFile( |
| 428 |
"Export to VCD", "VCD (*.vcd){.vcd}", |
| 429 |
[&](const auto& fn) { |
| 430 |
try { |
| 431 |
tracer.exportVCD(fn); |
| 432 |
} catch (MSXException& e) { |
| 433 |
manager.printError( |
| 434 |
"Couldn't export VCD: ", e.getMessage()); |
| 435 |
} |
| 436 |
}); |
| 437 |
} |
| 438 |
if (ImGui::MenuItem("Close")) show = false; |
| 439 |
}); |
| 440 |
im::Menu("View", [&]{ |
| 441 |
if (menuItemWithShortcut(manager, "Zoom to fit", Shortcuts::ID::TRACE_ZOOM_TO_FIT)) { |
| 442 |
zoomToFit(minT, totalT); |
| 443 |
} |
| 444 |
if (menuItemWithShortcut(manager, "Zoom in", Shortcuts::ID::TRACE_ZOOM_IN)) { |
| 445 |
doZoom(1.0 / 1.2, 0.5); |
| 446 |
} |
| 447 |
if (menuItemWithShortcut(manager, "Zoom out", Shortcuts::ID::TRACE_ZOOM_OUT)) { |
| 448 |
doZoom(1.2, 0.5); |
| 449 |
} |
| 450 |
ImGui::Separator(); |
| 451 |
|
| 452 |
ImGui::TextUnformatted("Timeline:"); |
| 453 |
im::Indent([&]{ |
| 454 |
im::Menu("origin", [&]{ |
| 455 |
ImGui::RadioButton("MSX boot", &timelineOrigin, Origin::ZERO); |
| 456 |
simpleToolTip("Absolute time since MSX boot"); |
| 457 |
ImGui::RadioButton("Primary marker", &timelineOrigin, Origin::PRIMARY); |
| 458 |
simpleToolTip("Relative to primary marker"); |
| 459 |
ImGui::RadioButton("Secondary marker", &timelineOrigin, Origin::SECONDARY); |
| 460 |
simpleToolTip("Relative to secondary marker"); |
| 461 |
ImGui::RadioButton("Current time", &timelineOrigin, Origin::NOW); |
| 462 |
simpleToolTip("Relative to current emulation time"); |
| 463 |
}); |
| 464 |
im::Menu("start", [&]{ |
| 465 |
if (ImGui::RadioButton("MSX boot", !timelineStart)) timelineStart = false; |
| 466 |
if (ImGui::RadioButton("First event", timelineStart)) timelineStart = true; |
| 467 |
}); |
| 468 |
im::Menu("stop", [&]{ |
| 469 |
if (ImGui::RadioButton("Current emulation time", !timelineStop)) timelineStop = false; |
| 470 |
if (ImGui::RadioButton("Last event", timelineStop)) timelineStop = true; |
| 471 |
}); |
| 472 |
ImGui::MenuItem("Track current time", nullptr, &followNow); |
| 473 |
simpleToolTip("Auto-scroll so that the current emulation time remains visible (only if it was already visible)"); |
| 474 |
}); |
| 475 |
ImGui::Separator(); |
| 476 |
|
| 477 |
menuItemWithShortcut(manager, "Show menu bar", Shortcuts::ID::TRACE_SHOW_MENU_BAR, &showMenuBar); |
| 478 |
}); |
| 479 |
im::Menu("Goto", [&]{ |
| 480 |
constexpr auto pos_text = "positive edge = rising edge = transition from low to high"; |
| 481 |
constexpr auto neg_text = "negative edge = falling edge = transition from high to low"; |
| 482 |
ImGui::TextUnformatted("Move primary marker to"); |
| 483 |
simpleToolTip("Place via left-mouse-click on the graph."); |
| 484 |
im::Indent([&]{ |
| 485 |
if (menuItemWithShortcut(manager, "Next edge", Shortcuts::ID::TRACE_NEXT_EDGE)) { |
| 486 |
gotoNextEdge(selectedTime1); |
| 487 |
} |
| 488 |
if (menuItemWithShortcut(manager, "Next positive edge", Shortcuts::ID::TRACE_NEXT_POS_EDGE)) { |
| 489 |
gotoNextPosEdge(selectedTime1); |
| 490 |
} |
| 491 |
simpleToolTip(pos_text); |
| 492 |
if (menuItemWithShortcut(manager, "Next negative edge", Shortcuts::ID::TRACE_NEXT_NEG_EDGE)) { |
| 493 |
gotoNextNegEdge(selectedTime1); |
| 494 |
} |
| 495 |
simpleToolTip(neg_text); |
| 496 |
if (menuItemWithShortcut(manager, "Previous edge", Shortcuts::ID::TRACE_PREV_EDGE)) { |
| 497 |
gotoPrevEdge(selectedTime1); |
| 498 |
} |
| 499 |
if (menuItemWithShortcut(manager, "Previous positive edge", Shortcuts::ID::TRACE_PREV_POS_EDGE)) { |
| 500 |
gotoPrevPosEdge(selectedTime1); |
| 501 |
} |
| 502 |
simpleToolTip(pos_text); |
| 503 |
if (menuItemWithShortcut(manager, "Previous negative edge", Shortcuts::ID::TRACE_PREV_NEG_EDGE)) { |
| 504 |
gotoPrevNegEdge(selectedTime1); |
| 505 |
} |
| 506 |
simpleToolTip(neg_text); |
| 507 |
}); |
| 508 |
ImGui::Separator(); |
| 509 |
|
| 510 |
ImGui::TextUnformatted("Move secondary marker to"); |
| 511 |
simpleToolTip("Place via shift-left-mouse-click on the graph."); |
| 512 |
im::Indent([&]{ |
| 513 |
if (menuItemWithShortcut(manager, "Next edge##alt", Shortcuts::ID::TRACE_ALT_NEXT_EDGE)) { |
| 514 |
gotoNextEdge(selectedTime2); |
| 515 |
} |
| 516 |
if (menuItemWithShortcut(manager, "Next positive edge##alt", Shortcuts::ID::TRACE_ALT_NEXT_POS_EDGE)) { |
| 517 |
gotoNextPosEdge(selectedTime2); |
| 518 |
} |
| 519 |
simpleToolTip(pos_text); |
| 520 |
if (menuItemWithShortcut(manager, "Next negative edge##alt", Shortcuts::ID::TRACE_ALT_NEXT_NEG_EDGE)) { |
| 521 |
gotoNextNegEdge(selectedTime2); |
| 522 |
} |
| 523 |
simpleToolTip(neg_text); |
| 524 |
if (menuItemWithShortcut(manager, "Previous edge##alt", Shortcuts::ID::TRACE_ALT_PREV_EDGE)) { |
| 525 |
gotoPrevEdge(selectedTime2); |
| 526 |
} |
| 527 |
if (menuItemWithShortcut(manager, "Previous positive edge##alt", Shortcuts::ID::TRACE_ALT_PREV_POS_EDGE)) { |
| 528 |
gotoPrevPosEdge(selectedTime2); |
| 529 |
} |
| 530 |
simpleToolTip(pos_text); |
| 531 |
if (menuItemWithShortcut(manager, "Previous negative edge##alt", Shortcuts::ID::TRACE_ALT_PREV_NEG_EDGE)) { |
| 532 |
gotoPrevNegEdge(selectedTime2); |
| 533 |
} |
| 534 |
simpleToolTip(neg_text); |
| 535 |
}); |
| 536 |
}); |
| 537 |
im::Menu("Help", [&]{ |
| 538 |
if (ImGui::MenuItem("Overview")) { |
| 539 |
showHelp = true; |
| 540 |
helpSection = HelpSection::OVERVIEW; |
| 541 |
} |
| 542 |
if (ImGui::MenuItem("Probes and Traces")) { |
| 543 |
showHelp = true; |
| 544 |
helpSection = HelpSection::PROBES_AND_TRACES; |
| 545 |
} |
| 546 |
if (ImGui::MenuItem("Graphs")) { |
| 547 |
showHelp = true; |
| 548 |
helpSection = HelpSection::GRAPHS; |
| 549 |
} |
| 550 |
if (ImGui::MenuItem("Navigation")) { |
| 551 |
showHelp = true; |
| 552 |
helpSection = HelpSection::NAVIGATION; |
| 553 |
} |
| 554 |
if (ImGui::MenuItem("Example")) { |
| 555 |
showHelp = true; |
| 556 |
helpSection = HelpSection::EXAMPLE; |
| 557 |
} |
| 558 |
}); |
| 559 |
} |
| 560 |
|
| 561 |
template<size_t N> |
| 562 |
static void thinWhiteLine(ImDrawList* drawList, const std::array<ImVec2, N>& points) |
| 563 |
requires(N == 2) |
| 564 |
{ |
| 565 |
auto color = getColor(imColor::TEXT); |
| 566 |
drawList->AddLine(points[0], points[1], color, 1.0f); |
| 567 |
drawList->AddPolyline(points.data(), narrow<int>(points.size()), color, 1.0f); |
| 568 |
} |
| 569 |
template<size_t N> |
| 570 |
static void thinWhiteLine(ImDrawList* drawList, const std::array<ImVec2, N>& points) |
| 571 |
requires(N != 2) |
| 572 |
{ |
| 573 |
auto color = getColor(imColor::TEXT); |
| 574 |
drawList->AddPolyline(points.data(), narrow<int>(points.size()), color, 1.0f); |
| 575 |
} |
| 576 |
static void arrowLeft(ImDrawList* drawList, gl::vec2 center, float u) |
| 577 |
{ |
| 578 |
std::array<ImVec2, 3> line1 = { |
| 579 |
center + u * gl::vec2{ 4.0f, -3.0f}, |
| 580 |
center + u * gl::vec2{ 1.0f, 0.0f}, |
| 581 |
center + u * gl::vec2{ 4.0f, 3.0f}, |
| 582 |
}; |
| 583 |
thinWhiteLine(drawList, line1); |
| 584 |
std::array<ImVec2, 2> line2 = { |
| 585 |
center + u * gl::vec2{ 1.0f, 0.0f}, |
| 586 |
center + u * gl::vec2{10.0f, 0.0f}, |
| 587 |
}; |
| 588 |
thinWhiteLine(drawList, line2); |
| 589 |
} |
| 590 |
static void arrowRight(ImDrawList* drawList, gl::vec2 center, float u) |
| 591 |
{ |
| 592 |
std::array<ImVec2, 3> line1 = { |
| 593 |
center + u * gl::vec2{-4.0f, -3.0f}, |
| 594 |
center + u * gl::vec2{-1.0f, 0.0f}, |
| 595 |
center + u * gl::vec2{-4.0f, 3.0f}, |
| 596 |
}; |
| 597 |
thinWhiteLine(drawList, line1); |
| 598 |
std::array<ImVec2, 2> line2 = { |
| 599 |
center + u * gl::vec2{- 1.0f, 0.0f}, |
| 600 |
center + u * gl::vec2{-10.0f, 0.0f}, |
| 601 |
}; |
| 602 |
thinWhiteLine(drawList, line2); |
| 603 |
} |
| 604 |
static void magnifyingGlass(ImDrawList* drawList, gl::vec2 center, float u) |
| 605 |
{ |
| 606 |
auto color = getColor(imColor::TEXT); |
| 607 |
drawList->AddCircle(center + u * gl::vec2{-1.0f, -1.0f}, 8.0f, color); |
| 608 |
std::array<ImVec2, 2> line = { |
| 609 |
center + u * gl::vec2{ 4.6f, 4.6f}, |
| 610 |
center + u * gl::vec2{ 9.0f, 9.0f}, |
| 611 |
}; |
| 612 |
thinWhiteLine(drawList, line); |
| 613 |
} |
| 614 |
|
| 615 |
static void RenderZoomFit(gl::vec2 center, ImDrawList* drawList) |
| 616 |
{ |
| 617 |
auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f); |
| 618 |
magnifyingGlass(drawList, center, u); |
| 619 |
std::array<ImVec2, 2> line1 = { |
| 620 |
center + u * gl::vec2{-6.0f, -1.0f}, |
| 621 |
center + u * gl::vec2{ 4.0f, -1.0f}, |
| 622 |
}; |
| 623 |
thinWhiteLine(drawList, line1); |
| 624 |
std::array<ImVec2, 3> line2 = { |
| 625 |
center + u * gl::vec2{-3.0f, -4.0f}, |
| 626 |
center + u * gl::vec2{-6.0f, -1.0f}, |
| 627 |
center + u * gl::vec2{-3.0f, 2.0f}, |
| 628 |
}; |
| 629 |
thinWhiteLine(drawList, line2); |
| 630 |
std::array<ImVec2, 3> line3 = { |
| 631 |
center + u * gl::vec2{ 1.0f, -4.0f}, |
| 632 |
center + u * gl::vec2{ 4.0f, -1.0f}, |
| 633 |
center + u * gl::vec2{ 1.0f, 2.0f}, |
| 634 |
}; |
| 635 |
thinWhiteLine(drawList, line3); |
| 636 |
} |
| 637 |
static void RenderZoomOut(gl::vec2 center, ImDrawList* drawList) |
| 638 |
{ |
| 639 |
auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f); |
| 640 |
magnifyingGlass(drawList, center, u); |
| 641 |
std::array<ImVec2, 2> line1 = { |
| 642 |
center + u * gl::vec2{-6.0f, -1.0f}, |
| 643 |
center + u * gl::vec2{ 4.0f, -1.0f}, |
| 644 |
}; |
| 645 |
thinWhiteLine(drawList, line1); |
| 646 |
} |
| 647 |
static void RenderZoomIn(gl::vec2 center, ImDrawList* drawList) |
| 648 |
{ |
| 649 |
auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f); |
| 650 |
magnifyingGlass(drawList, center, u); |
| 651 |
std::array<ImVec2, 2> line1 = { |
| 652 |
center + u * gl::vec2{-6.0f, -1.0f}, |
| 653 |
center + u * gl::vec2{ 4.0f, -1.0f}, |
| 654 |
}; |
| 655 |
thinWhiteLine(drawList, line1); |
| 656 |
std::array<ImVec2, 2> line2 = { |
| 657 |
center + u * gl::vec2{-1.0f, -6.0f}, |
| 658 |
center + u * gl::vec2{-1.0f, 4.0f}, |
| 659 |
}; |
| 660 |
thinWhiteLine(drawList, line2); |
| 661 |
} |
| 662 |
static void RenderPrevNegEdge(gl::vec2 center, ImDrawList* drawList) |
| 663 |
{ |
| 664 |
auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f); |
| 665 |
std::array<ImVec2, 4> line1 = { |
| 666 |
center + u * gl::vec2{-9.0f, -9.0f}, |
| 667 |
center + u * gl::vec2{-3.0f, -9.0f}, |
| 668 |
center + u * gl::vec2{-3.0f, 9.0f}, |
| 669 |
center + u * gl::vec2{ 3.0f, 9.0f}, |
| 670 |
}; |
| 671 |
thinWhiteLine(drawList, line1); |
| 672 |
arrowLeft(drawList, center, u); |
| 673 |
} |
| 674 |
static void RenderPrevPosEdge(gl::vec2 center, ImDrawList* drawList) |
| 675 |
{ |
| 676 |
auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f); |
| 677 |
std::array<ImVec2, 4> line1 = { |
| 678 |
center + u * gl::vec2{-9.0f, 9.0f}, |
| 679 |
center + u * gl::vec2{-3.0f, 9.0f}, |
| 680 |
center + u * gl::vec2{-3.0f, -9.0f}, |
| 681 |
center + u * gl::vec2{ 3.0f, -9.0f}, |
| 682 |
}; |
| 683 |
thinWhiteLine(drawList, line1); |
| 684 |
arrowLeft(drawList, center, u); |
| 685 |
} |
| 686 |
static void RenderPrevEdge(gl::vec2 center, ImDrawList* drawList) |
| 687 |
{ |
| 688 |
auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f); |
| 689 |
std::array<ImVec2, 4> line1 = { |
| 690 |
center + u * gl::vec2{-9.0f, 9.0f}, |
| 691 |
center + u * gl::vec2{-6.0f, 9.0f}, |
| 692 |
center + u * gl::vec2{ 0.0f, -9.0f}, |
| 693 |
center + u * gl::vec2{ 4.0f, -9.0f}, |
| 694 |
}; |
| 695 |
thinWhiteLine(drawList, line1); |
| 696 |
std::array<ImVec2, 4> line2 = { |
| 697 |
center + u * gl::vec2{-9.0f, -9.0f}, |
| 698 |
center + u * gl::vec2{-6.0f, -9.0f}, |
| 699 |
center + u * gl::vec2{ 0.0f, 9.0f}, |
| 700 |
center + u * gl::vec2{ 4.0f, 9.0f}, |
| 701 |
}; |
| 702 |
thinWhiteLine(drawList, line2); |
| 703 |
arrowLeft(drawList, center, u); |
| 704 |
} |
| 705 |
static void RenderNextEdge(gl::vec2 center, ImDrawList* drawList) |
| 706 |
{ |
| 707 |
auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f); |
| 708 |
std::array<ImVec2, 4> line1 = { |
| 709 |
center + u * gl::vec2{ 9.0f, 9.0f}, |
| 710 |
center + u * gl::vec2{ 6.0f, 9.0f}, |
| 711 |
center + u * gl::vec2{ 0.0f, -9.0f}, |
| 712 |
center + u * gl::vec2{-4.0f, -9.0f}, |
| 713 |
}; |
| 714 |
thinWhiteLine(drawList, line1); |
| 715 |
std::array<ImVec2, 4> line2 = { |
| 716 |
center + u * gl::vec2{ 9.0f, -9.0f}, |
| 717 |
center + u * gl::vec2{ 6.0f, -9.0f}, |
| 718 |
center + u * gl::vec2{ 0.0f, 9.0f}, |
| 719 |
center + u * gl::vec2{-4.0f, 9.0f}, |
| 720 |
}; |
| 721 |
thinWhiteLine(drawList, line2); |
| 722 |
arrowRight(drawList, center, u); |
| 723 |
} |
| 724 |
static void RenderNextNegEdge(gl::vec2 center, ImDrawList* drawList) |
| 725 |
{ |
| 726 |
auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f); |
| 727 |
std::array<ImVec2, 4> line1 = { |
| 728 |
center + u * gl::vec2{ 9.0f, 9.0f}, |
| 729 |
center + u * gl::vec2{ 3.0f, 9.0f}, |
| 730 |
center + u * gl::vec2{ 3.0f, -9.0f}, |
| 731 |
center + u * gl::vec2{-3.0f, -9.0f}, |
| 732 |
}; |
| 733 |
thinWhiteLine(drawList, line1); |
| 734 |
arrowRight(drawList, center, u); |
| 735 |
} |
| 736 |
static void RenderNextPosEdge(gl::vec2 center, ImDrawList* drawList) |
| 737 |
{ |
| 738 |
auto u = ImGui::GetFrameHeightWithSpacing() * (1.0f / 23.0f); |
| 739 |
std::array<ImVec2, 4> line1 = { |
| 740 |
center + u * gl::vec2{ 9.0f, -9.0f}, |
| 741 |
center + u * gl::vec2{ 3.0f, -9.0f}, |
| 742 |
center + u * gl::vec2{ 3.0f, 9.0f}, |
| 743 |
center + u * gl::vec2{-3.0f, 9.0f}, |
| 744 |
}; |
| 745 |
thinWhiteLine(drawList, line1); |
| 746 |
arrowRight(drawList, center, u); |
| 747 |
} |
| 748 |
|
| 749 |
void ImGuiTraceViewer::zoomToFit(EmuTime minT, EmuDuration totalT) |
| 750 |
{ |
| 751 |
viewStartTime = minT; |
| 752 |
viewDuration = totalT; |
| 753 |
scrollChanged = true; |
| 754 |
} |
| 755 |
|
| 756 |
void ImGuiTraceViewer::doZoom(double factor, double xFract) |
| 757 |
{ |
| 758 |
auto newDuration = std::max(viewDuration * factor, EmuDuration(100u)); |
| 759 |
auto realFactor = newDuration.div(viewDuration); |
| 760 |
|
| 761 |
if (realFactor > 1.0) { |
| 762 |
auto offset = viewDuration * (xFract * (realFactor - 1.0)); |
| 763 |
viewStartTime = viewStartTime.saturateSubtract(offset); |
| 764 |
} else { |
| 765 |
auto offset = viewDuration * (xFract * (1.0 - realFactor)); |
| 766 |
viewStartTime += offset; |
| 767 |
} |
| 768 |
|
| 769 |
viewDuration = newDuration; |
| 770 |
|
| 771 |
scrollChanged = true; |
| 772 |
} |
| 773 |
|
| 774 |
void ImGuiTraceViewer::scrollTo(EmuTime time) |
| 775 |
{ |
| 776 |
if (time < viewStartTime) { |
| 777 |
viewStartTime = time.saturateSubtract(viewDuration * 0.2); |
| 778 |
} else if (time >= viewStartTime + viewDuration) { |
| 779 |
viewStartTime = time.saturateSubtract(viewDuration * 0.8); |
| 780 |
} |
| 781 |
scrollChanged = true; |
| 782 |
} |
| 783 |
|
| 784 |
[[nodiscard]] static Tracer::Trace* getTrace(Traces traces, int row) |
| 785 |
{ |
| 786 |
if (row < 0 || row >= int(traces.size())) return nullptr; |
| 787 |
return traces[row]; |
| 788 |
} |
| 789 |
|
| 790 |
[[nodiscard]] double ImGuiTraceViewer::getUnitConversionFactor() const |
| 791 |
{ |
| 792 |
static_assert(NUM_UNITS == 6); |
| 793 |
switch (units) { |
| 794 |
default: |
| 795 |
case SECONDS: return 1.0; |
| 796 |
case MILLI_SECONDS: return 1'000.0; |
| 797 |
case MICRO_SECONDS: return 1'000'000.0; |
| 798 |
case CYCLES: return 3'579'545.0; |
| 799 |
case PAL: return (6 * 3'579'545.0) / (1368 * 313); |
| 800 |
case NTSC: return (6 * 3'579'545.0) / (1368 * 262); |
| 801 |
} |
| 802 |
} |
| 803 |
|
| 804 |
[[nodiscard]] static const Tracer::Event* findStrictlySmaller(const Tracer::Trace& trace, EmuTime time) |
| 805 |
{ |
| 806 |
auto it = std::ranges::lower_bound(trace.events, time, {}, &Tracer::Event::time); |
| 807 |
if (it == trace.events.begin()) return nullptr; |
| 808 |
return std::to_address(it - 1); |
| 809 |
} |
| 810 |
|
| 811 |
[[nodiscard]] static const Tracer::Event* findStrictlyBigger(const Tracer::Trace& trace, EmuTime time) |
| 812 |
{ |
| 813 |
auto it = std::ranges::upper_bound(trace.events, time, {}, &Tracer::Event::time); |
| 814 |
if (it == trace.events.end()) return nullptr; |
| 815 |
return std::to_address(it); |
| 816 |
} |
| 817 |
|
| 818 |
void ImGuiTraceViewer::gotoPrevNegEdge(EmuTime& selectedTime) |
| 819 |
{ |
| 820 |
if (const auto* trace = getTrace(traces, selectedRow)) { |
| 821 |
auto time = selectedTime; |
| 822 |
while (true) { |
| 823 |
const auto* event = findStrictlySmaller(*trace, time); |
| 824 |
if (!event) return; |
| 825 |
time = event->time; |
| 826 |
if (!trace->isBool() || event->value.get_u64() == 0) break; |
| 827 |
} |
| 828 |
selectedTime = time; |
| 829 |
scrollTo(time); |
| 830 |
} |
| 831 |
} |
| 832 |
|
| 833 |
void ImGuiTraceViewer::gotoPrevPosEdge(EmuTime& selectedTime) |
| 834 |
{ |
| 835 |
if (const auto* trace = getTrace(traces, selectedRow)) { |
| 836 |
auto time = selectedTime; |
| 837 |
while (true) { |
| 838 |
const auto* event = findStrictlySmaller(*trace, time); |
| 839 |
if (!event) return; |
| 840 |
time = event->time; |
| 841 |
if (!trace->isBool() || event->value.get_u64() != 0) break; |
| 842 |
} |
| 843 |
selectedTime = time; |
| 844 |
scrollTo(time); |
| 845 |
} |
| 846 |
} |
| 847 |
|
| 848 |
void ImGuiTraceViewer::gotoPrevEdge(EmuTime& selectedTime) |
| 849 |
{ |
| 850 |
if (const auto* trace = getTrace(traces, selectedRow)) { |
| 851 |
if (const auto* event = findStrictlySmaller(*trace, selectedTime)) { |
| 852 |
selectedTime = event->time; |
| 853 |
scrollTo(event->time); |
| 854 |
} |
| 855 |
} |
| 856 |
} |
| 857 |
|
| 858 |
void ImGuiTraceViewer::gotoNextEdge(EmuTime& selectedTime) |
| 859 |
{ |
| 860 |
if (const auto* trace = getTrace(traces, selectedRow)) { |
| 861 |
if (const auto* event = findStrictlyBigger(*trace, selectedTime)) { |
| 862 |
selectedTime = event->time; |
| 863 |
scrollTo(event->time); |
| 864 |
} |
| 865 |
} |
| 866 |
} |
| 867 |
|
| 868 |
void ImGuiTraceViewer::gotoNextNegEdge(EmuTime& selectedTime) |
| 869 |
{ |
| 870 |
if (const auto* trace = getTrace(traces, selectedRow)) { |
| 871 |
auto time = selectedTime; |
| 872 |
while (true) { |
| 873 |
const auto* event = findStrictlyBigger(*trace, time); |
| 874 |
if (!event) return; |
| 875 |
time = event->time; |
| 876 |
if (!trace->isBool() || event->value.get_u64() == 0) break; |
| 877 |
} |
| 878 |
selectedTime = time; |
| 879 |
scrollTo(time); |
| 880 |
} |
| 881 |
} |
| 882 |
|
| 883 |
void ImGuiTraceViewer::gotoNextPosEdge(EmuTime& selectedTime) |
| 884 |
{ |
| 885 |
if (const auto* trace = getTrace(traces, selectedRow)) { |
| 886 |
auto time = selectedTime; |
| 887 |
while (true) { |
| 888 |
const auto* event = findStrictlyBigger(*trace, time); |
| 889 |
if (!event) return; |
| 890 |
time = event->time; |
| 891 |
if (!trace->isBool() || event->value.get_u64() != 0) break; |
| 892 |
} |
| 893 |
selectedTime = time; |
| 894 |
scrollTo(time); |
| 895 |
} |
| 896 |
} |
| 897 |
|
| 898 |
static void toolTipWithShortcut(ImGuiManager& manager, const char* text, Shortcuts::ID id) |
| 899 |
{ |
| 900 |
simpleToolTip([&] -> std::string { |
| 901 |
const auto& shortcuts = manager.getShortcuts(); |
| 902 |
const auto& shortcut = shortcuts.getShortcut(id); |
| 903 |
if (shortcut.keyChord == ImGuiKey_None) return std::string(text); |
| 904 |
return strCat(text, " (", getKeyChordName(shortcut.keyChord), ')'); |
| 905 |
}); |
| 906 |
} |
| 907 |
|
| 908 |
[[nodiscard]] static std::string formatDouble(double value) |
| 909 |
{ |
| 910 |
std::array<char, 32> buffer; |
| 911 |
#if 0 |
| 912 |
|
| 913 |
|
| 914 |
|
| 915 |
|
| 916 |
|
| 917 |
|
| 918 |
#endif |
| 919 |
int len = snprintf(buffer.data(), buffer.size(), "%.11g", value); |
| 920 |
assert(len > 0 && size_t(len) < buffer.size()); |
| 921 |
return std::string(buffer.data(), len); |
| 922 |
} |
| 923 |
|
| 924 |
void ImGuiTraceViewer::drawToolBar(EmuTime minT, EmuDuration totalT, float viewScreenWidth) |
| 925 |
{ |
| 926 |
if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) { |
| 927 |
if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) { |
| 928 |
auto& io = ImGui::GetIO(); |
| 929 |
if (io.MouseWheel != 0.0f) { |
| 930 |
auto mx = io.MousePos.x - (ImGui::GetCursorScreenPos().x + col0_width + splitterWidth); |
| 931 |
auto xFract = mx / viewScreenWidth; |
| 932 |
doZoom(std::pow(1.2, double(-io.MouseWheel)), double(xFract)); |
| 933 |
io.MouseWheel = 0.0f; |
| 934 |
} |
| 935 |
} |
| 936 |
} |
| 937 |
|
| 938 |
using enum Shortcuts::ID; |
| 939 |
const auto& shortcuts = manager.getShortcuts(); |
| 940 |
if (shortcuts.checkShortcut(TRACE_SHOW_MENU_BAR)) { |
| 941 |
showMenuBar = !showMenuBar; |
| 942 |
} |
| 943 |
const auto& style = ImGui::GetStyle(); |
| 944 |
gl::vec2 size{ImGui::GetFrameHeightWithSpacing()}; |
| 945 |
if (ButtonWithCustomRendering("##ZoomFit", size, false, RenderZoomFit) || |
| 946 |
shortcuts.checkShortcut(TRACE_ZOOM_TO_FIT)) { |
| 947 |
zoomToFit(minT, totalT); |
| 948 |
} |
| 949 |
toolTipWithShortcut(manager, "Zoom to fit", TRACE_ZOOM_TO_FIT); |
| 950 |
ImGui::SameLine(); |
| 951 |
|
| 952 |
if (ButtonWithCustomRendering("##ZoomOut", size, false, RenderZoomOut) || |
| 953 |
shortcuts.checkShortcut(TRACE_ZOOM_OUT)) { |
| 954 |
doZoom(1.2, 0.5); |
| 955 |
} |
| 956 |
toolTipWithShortcut(manager, "Zoom out", TRACE_ZOOM_OUT); |
| 957 |
ImGui::SameLine(); |
| 958 |
|
| 959 |
if (ButtonWithCustomRendering("##ZoomIn", size, false, RenderZoomIn) || |
| 960 |
shortcuts.checkShortcut(TRACE_ZOOM_IN)) { |
| 961 |
doZoom(1.0 / 1.2, 0.5); |
| 962 |
} |
| 963 |
toolTipWithShortcut(manager, "Zoom in", TRACE_ZOOM_IN); |
| 964 |
ImGui::SameLine(0.0f, style.ItemSpacing.x * 2.0f); |
| 965 |
|
| 966 |
bool shift = ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift); |
| 967 |
auto gotoStuff = [&](const char* label, const char* tooltip, |
| 968 |
Shortcuts::ID ID, Shortcuts::ID altID, |
| 969 |
auto renderFunc, auto action) { |
| 970 |
if (ButtonWithCustomRendering(label, size, false, renderFunc)) { |
| 971 |
std::invoke(action, this, shift ? selectedTime2 : selectedTime1); |
| 972 |
} |
| 973 |
toolTipWithShortcut(manager, tooltip, shift ? altID : ID); |
| 974 |
if (shortcuts.checkShortcut(ID)) { |
| 975 |
std::invoke(action, this, selectedTime1); |
| 976 |
} |
| 977 |
if (shortcuts.checkShortcut(altID)) { |
| 978 |
std::invoke(action, this, selectedTime2); |
| 979 |
} |
| 980 |
}; |
| 981 |
gotoStuff("##PrevNegEdge", "Goto previous negative edge", |
| 982 |
TRACE_PREV_NEG_EDGE, TRACE_ALT_PREV_NEG_EDGE, |
| 983 |
RenderPrevNegEdge, &ImGuiTraceViewer::gotoPrevNegEdge); |
| 984 |
ImGui::SameLine(); |
| 985 |
gotoStuff("##PrevPosEdge", "Goto previous positive edge", |
| 986 |
TRACE_PREV_POS_EDGE, TRACE_ALT_PREV_POS_EDGE, |
| 987 |
RenderPrevPosEdge, &ImGuiTraceViewer::gotoPrevPosEdge); |
| 988 |
ImGui::SameLine(); |
| 989 |
gotoStuff("##PrevEdge", "Goto previous edge (positive or negative)", |
| 990 |
TRACE_PREV_EDGE, TRACE_ALT_PREV_EDGE, |
| 991 |
RenderPrevEdge, &ImGuiTraceViewer::gotoPrevEdge); |
| 992 |
ImGui::SameLine(); |
| 993 |
gotoStuff("##NextEdge", "Goto next edge (positive or negative)", |
| 994 |
TRACE_NEXT_EDGE, TRACE_ALT_NEXT_EDGE, |
| 995 |
RenderNextEdge, &ImGuiTraceViewer::gotoNextEdge); |
| 996 |
ImGui::SameLine(); |
| 997 |
gotoStuff("##NextPosEdge", "Goto next positive edge", |
| 998 |
TRACE_NEXT_POS_EDGE, TRACE_ALT_NEXT_POS_EDGE, |
| 999 |
RenderNextPosEdge, &ImGuiTraceViewer::gotoNextPosEdge); |
| 1000 |
ImGui::SameLine(); |
| 1001 |
gotoStuff("##NextNegEdge", "Goto next negative edge", |
| 1002 |
TRACE_NEXT_NEG_EDGE, TRACE_ALT_NEXT_NEG_EDGE, |
| 1003 |
RenderNextNegEdge, &ImGuiTraceViewer::gotoNextNegEdge); |
| 1004 |
HelpMarker("Place a primary (or secondary) marker by clicking (or shift-click) on the graph below.\n" |
| 1005 |
"Advance the markers to the previous/next (positive or negative) edge via the buttons on the left, " |
| 1006 |
"(normal-click for the primary marker, shift-click for the secondary marker).\n" |
| 1007 |
"On the right the position and distance between the markers is shown. " |
| 1008 |
"The units can be selected via the dropdown list in the top-right corner."); |
| 1009 |
ImGui::SameLine(0.0f, style.ItemSpacing.x * 2.0f); |
| 1010 |
|
| 1011 |
auto unitFactor = getUnitConversionFactor(); |
| 1012 |
if (timelineOrigin == one_of(Origin::ZERO, Origin::NOW)) { |
| 1013 |
ImGui::TextUnformatted("prim."sv); |
| 1014 |
simpleToolTip("Primary marker (green). Place via left-mouse-click on the graph."); |
| 1015 |
ImGui::SameLine(); |
| 1016 |
auto leftStr = formatDouble(selectedTime1.toDouble() * unitFactor); |
| 1017 |
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8.0f); |
| 1018 |
if (ImGui::InputText("##left", &leftStr)) { |
| 1019 |
if (auto d = StringOp::stringTo<double>(leftStr)) { |
| 1020 |
selectedTime1 = EmuTime::fromDouble(*d / unitFactor); |
| 1021 |
} |
| 1022 |
} |
| 1023 |
ImGui::SameLine(); |
| 1024 |
ImGui::TextUnformatted("sec."sv); |
| 1025 |
simpleToolTip("Secondary marker (red). Place via shift-left-mouse-click on the graph."); |
| 1026 |
ImGui::SameLine(); |
| 1027 |
auto rightStr = formatDouble(selectedTime2.toDouble() * unitFactor); |
| 1028 |
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8.0f); |
| 1029 |
if (ImGui::InputText("##right", &rightStr)) { |
| 1030 |
if (auto d = StringOp::stringTo<double>(rightStr)) { |
| 1031 |
selectedTime2 = EmuTime::fromDouble(*d / unitFactor); |
| 1032 |
} |
| 1033 |
} |
| 1034 |
ImGui::SameLine(); |
| 1035 |
} |
| 1036 |
ImGui::TextUnformatted("dist."sv); |
| 1037 |
simpleToolTip("Distance between primary and secondary markers."); |
| 1038 |
ImGui::SameLine(); |
| 1039 |
im::Disabled(true, [&]{ |
| 1040 |
auto distStr = formatDouble((selectedTime2.toDouble() - selectedTime1.toDouble()) * unitFactor); |
| 1041 |
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8.0f); |
| 1042 |
ImGui::InputText("##dist", &distStr, ImGuiInputTextFlags_ReadOnly); |
| 1043 |
}); |
| 1044 |
ImGui::SameLine(); |
| 1045 |
|
| 1046 |
ImGui::TextUnformatted("units"sv); |
| 1047 |
ImGui::SameLine(); |
| 1048 |
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 5.0f); |
| 1049 |
static_assert(NUM_UNITS == 6); |
| 1050 |
std::array shortUnitNames = { |
| 1051 |
"sec", "ms", "\u00b5s", |
| 1052 |
"cycles", "PAL", "NTSC", |
| 1053 |
}; |
| 1054 |
std::array longUnitNames = { |
| 1055 |
"sec (seconds)", "ms (milliseconds)", "\u00b5s (microseconds)", |
| 1056 |
"cycles (Z80 @3.57MHz)", "PAL frames (50.16Hz)", "NTSC frames (59.92Hz)", |
| 1057 |
}; |
| 1058 |
im::Combo("##units", shortUnitNames[units], [&]{ |
| 1059 |
for (int i = 0; i < Units::NUM_UNITS; ++i) { |
| 1060 |
if (ImGui::Selectable(longUnitNames[i])) { |
| 1061 |
units = i; |
| 1062 |
} |
| 1063 |
} |
| 1064 |
}); |
| 1065 |
} |
| 1066 |
|
| 1067 |
void ImGuiTraceViewer::drawNames(float rulerHeight, float rowHeight, int mouseRow) |
| 1068 |
{ |
| 1069 |
int windowFlags = ImGuiWindowFlags_NoScrollbar |
| 1070 |
| ImGuiWindowFlags_NoScrollWithMouse |
| 1071 |
| ImGuiWindowFlags_AlwaysHorizontalScrollbar; |
| 1072 |
im::Child("##col0", gl::vec2{col0_width, 0.0f}, 0, windowFlags, [&] { |
| 1073 |
const auto& style = ImGui::GetStyle(); |
| 1074 |
const auto& io = ImGui::GetIO(); |
| 1075 |
|
| 1076 |
auto pos0 = ImGui::GetCursorPos(); |
| 1077 |
if (ImGui::Button("Select ...")) showSelect = true; |
| 1078 |
ImGui::SetCursorPos(pos0); |
| 1079 |
|
| 1080 |
ImGui::Dummy({10.0f, rulerHeight}); |
| 1081 |
auto cPos = ImGui::GetCursorPos(); |
| 1082 |
ImGui::SetScrollY(scrollY); |
| 1083 |
|
| 1084 |
auto* drawList = ImGui::GetWindowDrawList(); |
| 1085 |
gl::vec2 clipMin = drawList->GetClipRectMin() + gl::vec2{0.0f, rulerHeight}; |
| 1086 |
gl::vec2 clipMax = drawList->GetClipRectMax(); |
| 1087 |
bool hovered = (clipMin.x <= io.MousePos.x) && (io.MousePos.x < clipMax.x) && |
| 1088 |
(clipMin.y <= io.MousePos.y) && (io.MousePos.y < clipMax.y); |
| 1089 |
|
| 1090 |
std::optional<int> dragUp; |
| 1091 |
std::optional<int> dragDown; |
| 1092 |
bool openContext = false; |
| 1093 |
drawList->PushClipRect(clipMin, clipMax); |
| 1094 |
auto colorText = getColor(imColor::TEXT); |
| 1095 |
im::ListClipper(traces.size(), -1, rowHeight, [&](int row) { |
| 1096 |
auto tl = clipMin + gl::vec2{0.0f, float(row) * rowHeight}; |
| 1097 |
bool rowHovered = hovered && (row == mouseRow); |
| 1098 |
if (auto color = rowHovered ? std::optional<ImU32>(ImGui::GetColorU32(ImGuiCol_HeaderHovered)) |
| 1099 |
: (row == selectedRow) ? std::optional<ImU32>(ImGui::GetColorU32(ImGuiCol_Header)) |
| 1100 |
: std::optional<ImU32>{}) { |
| 1101 |
drawList->AddRectFilled(tl, tl + gl::vec2{col0_width, rowHeight}, *color); |
| 1102 |
} |
| 1103 |
const auto* trace = traces[row]; |
| 1104 |
const auto& name = trace->name; |
| 1105 |
drawText(drawList, tl + gl::vec2{0.0f, style.FramePadding.y + style.ItemSpacing.y}, |
| 1106 |
colorText, traces[row]->name); |
| 1107 |
|
| 1108 |
ImGui::SetCursorPos(cPos + gl::vec2{0.0f, float(row) * rowHeight}); |
| 1109 |
ImGui::InvisibleButton(name.c_str(), gl::vec2{col0_width, rowHeight}); |
| 1110 |
if (ImGui::IsItemActive() && !ImGui::IsItemHovered()) { |
| 1111 |
auto dy = ImGui::GetMouseDragDelta(0).y; |
| 1112 |
auto dist = rowHeight * 0.5f; |
| 1113 |
if (dy > dist) { |
| 1114 |
dragDown = row; |
| 1115 |
} else if (dy < -dist) { |
| 1116 |
dragUp = row; |
| 1117 |
} |
| 1118 |
} |
| 1119 |
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { |
| 1120 |
selectedRow = row; |
| 1121 |
} |
| 1122 |
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { |
| 1123 |
selectedRow = row; |
| 1124 |
openContext = true; |
| 1125 |
} |
| 1126 |
simpleToolTip(trace->description); |
| 1127 |
}); |
| 1128 |
drawList->PopClipRect(); |
| 1129 |
|
| 1130 |
auto doSwap = [&](int idx1, int idx2) { |
| 1131 |
assert(idx1 < idx2); |
| 1132 |
auto name1 = traces[idx1]->name; |
| 1133 |
auto name2 = traces[idx2]->name; |
| 1134 |
auto it1 = std::ranges::find(selectedTraces, name1, &SelectedTrace::name); |
| 1135 |
auto it2 = std::ranges::find(it1 + 1, selectedTraces.end(), name2, &SelectedTrace::name); |
| 1136 |
if (it1 != selectedTraces.end() && it2 != selectedTraces.end()) { |
| 1137 |
std::iter_swap(it1, it2); |
| 1138 |
} |
| 1139 |
}; |
| 1140 |
if (dragUp && *dragUp >= 1) { |
| 1141 |
doSwap(*dragUp - 1, *dragUp); |
| 1142 |
if (selectedRow == *dragUp) --selectedRow; |
| 1143 |
} else if (dragDown && *dragDown < int(traces.size() - 1)) { |
| 1144 |
doSwap(*dragDown, *dragDown + 1); |
| 1145 |
if (selectedRow == *dragDown) ++selectedRow; |
| 1146 |
} |
| 1147 |
|
| 1148 |
if (openContext) ImGui::OpenPopup("names-context"); |
| 1149 |
im::Popup("names-context", [&]{ |
| 1150 |
auto* trace = getTrace(traces, selectedRow); |
| 1151 |
if (!trace) { |
| 1152 |
ImGui::CloseCurrentPopup(); |
| 1153 |
return; |
| 1154 |
} |
| 1155 |
if (trace->getType() == Tracer::Trace::Type::INTEGER) { |
| 1156 |
im::Menu("Format", [&]{ |
| 1157 |
using enum Tracer::Trace::Format; |
| 1158 |
auto format = trace->getFormat(); |
| 1159 |
if (ImGui::RadioButton("Binary", format == BIN)) trace->setFormat(BIN); |
| 1160 |
if (ImGui::RadioButton("Decimal", format == DEC)) trace->setFormat(DEC); |
| 1161 |
if (ImGui::RadioButton("Hexadecimal", format == HEX)) trace->setFormat(HEX); |
| 1162 |
}); |
| 1163 |
ImGui::Separator(); |
| 1164 |
} |
| 1165 |
if (ImGui::MenuItem("Clear")) { |
| 1166 |
trace->clear(); |
| 1167 |
} |
| 1168 |
if (ImGui::MenuItem("Clear all")) { |
| 1169 |
for (auto* tr : traces) { |
| 1170 |
tr->clear(); |
| 1171 |
} |
| 1172 |
} |
| 1173 |
}); |
| 1174 |
}); |
| 1175 |
} |
| 1176 |
|
| 1177 |
void ImGuiTraceViewer::drawSplitter(float width) |
| 1178 |
{ |
| 1179 |
const auto& io = ImGui::GetIO(); |
| 1180 |
|
| 1181 |
ImGui::SameLine(0.0f, 0.0f); |
| 1182 |
|
| 1183 |
auto pos = ImGui::GetCursorScreenPos(); |
| 1184 |
auto avail = ImGui::GetContentRegionAvail(); |
| 1185 |
ImGui::InvisibleButton("##splitter", ImVec2(width, -1)); |
| 1186 |
bool hovered = ImGui::IsItemHovered(); |
| 1187 |
bool active = ImGui::IsItemActive(); |
| 1188 |
if (active && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { |
| 1189 |
col0_width = std::clamp(col0_width + io.MouseDelta.x, 50.0f, ImGui::GetContentRegionAvail().x - 50.0f); |
| 1190 |
} |
| 1191 |
if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW); |
| 1192 |
|
| 1193 |
auto x_center = pos.x + (width - 1.0f) * 0.5f; |
| 1194 |
auto y0 = pos.y; |
| 1195 |
auto y1 = y0 + avail.y; |
| 1196 |
|
| 1197 |
auto col_sep = ImGui::GetColorU32(ImGuiCol_Separator); |
| 1198 |
auto col_sep_active = ImGui::GetColorU32(ImGuiCol_SeparatorActive); |
| 1199 |
auto col_hover = ImGui::GetColorU32(ImGuiCol_SeparatorHovered); |
| 1200 |
ImGui::GetWindowDrawList()->AddLine( |
| 1201 |
ImVec2(x_center, y0), ImVec2(x_center, y1), active ? col_sep_active : (hovered ? col_hover : col_sep), 1.0f); |
| 1202 |
|
| 1203 |
ImGui::SameLine(0.0f, 0.0f); |
| 1204 |
} |
| 1205 |
|
| 1206 |
void ImGuiTraceViewer::drawRuler(gl::vec2 size, const Convertor& convertor, EmuTime from, EmuTime to, EmuTime now) |
| 1207 |
{ |
| 1208 |
auto labelSize = ImGui::CalcTextSize("0123456789"sv); |
| 1209 |
auto numLabels = size.x / labelSize.x; |
| 1210 |
auto origin = [&]{ |
| 1211 |
switch (timelineOrigin) { |
| 1212 |
default: |
| 1213 |
case Origin::ZERO: return 0.0; |
| 1214 |
case Origin::PRIMARY: return selectedTime1.toDouble(); |
| 1215 |
case Origin::SECONDARY: return selectedTime2.toDouble(); |
| 1216 |
case Origin::NOW: return now.toDouble(); |
| 1217 |
} |
| 1218 |
}(); |
| 1219 |
auto unitFactor = getUnitConversionFactor(); |
| 1220 |
auto [labels, step] = timelineFormatter.calc( |
| 1221 |
from.toDouble() - origin, to.toDouble() - origin, double(numLabels), unitFactor); |
| 1222 |
auto minorStep = step * 0.1 / unitFactor; |
| 1223 |
|
| 1224 |
auto* drawList = ImGui::GetWindowDrawList(); |
| 1225 |
auto colorText = getColor(imColor::TEXT); |
| 1226 |
auto colorTick = ImGui::GetColorU32(ImGuiCol_Separator); |
| 1227 |
auto y0 = size.y * 0.67f; |
| 1228 |
auto y1 = size.y * 0.85f; |
| 1229 |
auto y2 = size.y; |
| 1230 |
gl::vec2 clipMin = drawList->GetClipRectMin(); |
| 1231 |
gl::vec2 clipMax = drawList->GetClipRectMax(); |
| 1232 |
drawList->AddLine({clipMin.x, clipMin.y + y0}, {clipMax.x, clipMin.y + y0}, colorTick); |
| 1233 |
|
| 1234 |
for (const auto& l : labels) { |
| 1235 |
const auto& text = l.text; |
| 1236 |
auto value = l.value + origin; |
| 1237 |
auto textSize = ImGui::CalcTextSize(text); |
| 1238 |
auto centerX = convertor.timeToX(EmuTime::fromDouble(value)); |
| 1239 |
drawText(drawList, clipMin + gl::vec2{centerX - textSize.x * 0.5f, scrollY}, colorText, text); |
| 1240 |
for (auto i : xrange(10)) { |
| 1241 |
auto x = convertor.timeToX(EmuTime::fromDouble(value + double(i) * minorStep)); |
| 1242 |
drawList->AddLine(clipMin + gl::vec2{x, y0}, clipMin + gl::vec2{x, (i % 5) == 0 ? y2 : y1}, colorTick); |
| 1243 |
} |
| 1244 |
} |
| 1245 |
} |
| 1246 |
|
| 1247 |
static EmuTime snapToEvent(EmuTime time, auto convertor, std::span<const Tracer::Event> events) |
| 1248 |
{ |
| 1249 |
auto [it, dist] = find_closest(events, time, {}, &Tracer::Event::time); |
| 1250 |
if (it == events.end()) return time; |
| 1251 |
auto h5 = ImGui::GetFrameHeight() * 0.20f; |
| 1252 |
return (dist < convertor.deltaXtoDuration(h5)) ? it->time : time; |
| 1253 |
} |
| 1254 |
static EmuTime snapToEvent(float mouseX, auto convertor, std::span<const Tracer::Event> events) |
| 1255 |
{ |
| 1256 |
auto time = convertor.xToTime(mouseX); |
| 1257 |
return snapToEvent(time, convertor, events); |
| 1258 |
} |
| 1259 |
|
| 1260 |
void ImGuiTraceViewer::drawGraphs(float rulerHeight, float rowHeight, int mouseRow, const Convertor& convertor, |
| 1261 |
EmuTime minT, EmuTime maxT, EmuTime now, MSXMotherBoard& motherBoard) |
| 1262 |
{ |
| 1263 |
if (ImGui::Shortcut(ImGuiMod_Shift | ImGuiKey_LeftArrow, ImGuiInputFlags_Repeat)) { |
| 1264 |
viewStartTime = viewStartTime.saturateSubtract(convertor.deltaXtoDuration(0.5f * convertor.viewScreenWidth)); |
| 1265 |
scrollChanged = true; |
| 1266 |
} else if (ImGui::Shortcut(ImGuiMod_Shift | ImGuiKey_RightArrow, ImGuiInputFlags_Repeat)) { |
| 1267 |
viewStartTime += convertor.deltaXtoDuration(0.5f * convertor.viewScreenWidth); |
| 1268 |
scrollChanged = true; |
| 1269 |
} else if (ImGui::Shortcut(ImGuiKey_LeftArrow, ImGuiInputFlags_Repeat)) { |
| 1270 |
viewStartTime = viewStartTime.saturateSubtract(convertor.deltaXtoDuration(12.0f)); |
| 1271 |
scrollChanged = true; |
| 1272 |
} else if (ImGui::Shortcut(ImGuiKey_RightArrow, ImGuiInputFlags_Repeat)) { |
| 1273 |
viewStartTime += convertor.deltaXtoDuration(12.0f); |
| 1274 |
scrollChanged = true; |
| 1275 |
} |
| 1276 |
|
| 1277 |
auto totalT = maxT - minT; |
| 1278 |
auto total1000 = totalT * 1e-3; |
| 1279 |
auto [scrollWidth, scrollPos] = [&]{ |
| 1280 |
|
| 1281 |
if (total1000 < viewDuration) { |
| 1282 |
auto scrollSlackDuration = (viewDuration < totalT) ? (totalT - viewDuration) : EmuDuration::zero(); |
| 1283 |
auto width = float(scrollSlackDuration.div(viewDuration)) + 1.0f; |
| 1284 |
auto pos = float((viewStartTime - minT).div(viewDuration)); |
| 1285 |
return std::pair(width, pos); |
| 1286 |
} else { |
| 1287 |
auto width = 1000.0f; |
| 1288 |
auto pos = float((viewStartTime - minT).div(total1000)); |
| 1289 |
return std::pair(width, pos); |
| 1290 |
} |
| 1291 |
}(); |
| 1292 |
float fakeScrollWidth = scrollWidth * convertor.viewScreenWidth; |
| 1293 |
float fakeScrollPos = scrollPos * convertor.viewScreenWidth; |
| 1294 |
|
| 1295 |
fakeScrollWidth = std::max(fakeScrollWidth, fakeScrollPos + convertor.viewScreenWidth + 5); |
| 1296 |
ImGui::SetNextWindowContentSize({fakeScrollWidth, rulerHeight + rowHeight * float(traces.size())}); |
| 1297 |
ImGui::SetNextWindowScroll({fakeScrollPos, scrollY}); |
| 1298 |
|
| 1299 |
int windowFlags2 = ImGuiWindowFlags_HorizontalScrollbar |
| 1300 |
| ImGuiWindowFlags_AlwaysHorizontalScrollbar; |
| 1301 |
im::Child("Events", {}, {}, windowFlags2, [&] { |
| 1302 |
scrollY = ImGui::GetScrollY(); |
| 1303 |
auto scrollX = ImGui::GetScrollX(); |
| 1304 |
if (std::abs(scrollX - fakeScrollPos) > 0.5f) { |
| 1305 |
|
| 1306 |
auto dur = std::max(total1000, viewDuration); |
| 1307 |
viewStartTime = minT + (dur * double(scrollX / convertor.viewScreenWidth)); |
| 1308 |
scrollChanged = true; |
| 1309 |
} |
| 1310 |
|
| 1311 |
const auto& style = ImGui::GetStyle(); |
| 1312 |
const auto& io = ImGui::GetIO(); |
| 1313 |
|
| 1314 |
auto t0 = convertor.xToTime(0.0f); |
| 1315 |
auto t1 = convertor.xToTime(convertor.viewScreenWidth); |
| 1316 |
|
| 1317 |
drawRuler({convertor.viewScreenWidth, rulerHeight}, convertor, t0, t1, now); |
| 1318 |
ImGui::Dummy({10.0f, rulerHeight}); |
| 1319 |
|
| 1320 |
auto* drawList = ImGui::GetWindowDrawList(); |
| 1321 |
gl::vec2 clipMin = drawList->GetClipRectMin() + gl::vec2{0.0f, rulerHeight}; |
| 1322 |
gl::vec2 clipMax = drawList->GetClipRectMax(); |
| 1323 |
bool hovered = (clipMin.x <= io.MousePos.x) && (io.MousePos.x < clipMax.x) && |
| 1324 |
(clipMin.y <= io.MousePos.y) && (io.MousePos.y < clipMax.y) && |
| 1325 |
ImGui::IsWindowHovered(); |
| 1326 |
auto mouseX = io.MousePos.x - clipMin.x; |
| 1327 |
|
| 1328 |
drawList->PushClipRect(clipMin, clipMax); |
| 1329 |
im::ListClipperID(traces.size(), rowHeight, [&](int row) { |
| 1330 |
const auto& trace = *traces[row]; |
| 1331 |
auto it0 = std::ranges::lower_bound(trace.events, viewStartTime, {}, &Tracer::Event::time); |
| 1332 |
if (it0 != trace.events.begin()) --it0; |
| 1333 |
auto graphEndTime = viewStartTime + viewDuration; |
| 1334 |
auto it1 = std::ranges::upper_bound(trace.events, graphEndTime, {}, &Tracer::Event::time); |
| 1335 |
std::span<const Tracer::Event> visibleEvents(it0, it1); |
| 1336 |
|
| 1337 |
bool rowHovered = hovered && (row == mouseRow); |
| 1338 |
if (rowHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { |
| 1339 |
if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { |
| 1340 |
selectedTime2 = snapToEvent(mouseX, convertor, visibleEvents); |
| 1341 |
} else { |
| 1342 |
selectedRow = row; |
| 1343 |
selectedTime1 = snapToEvent(mouseX, convertor, visibleEvents); |
| 1344 |
} |
| 1345 |
} |
| 1346 |
auto tl = clipMin + gl::vec2{0.0f, float(row) * rowHeight}; |
| 1347 |
if (auto color = rowHovered ? std::optional<ImU32>(ImGui::GetColorU32(ImGuiCol_HeaderHovered)) |
| 1348 |
: (row == selectedRow) ? std::optional<ImU32>(ImGui::GetColorU32(ImGuiCol_Header)) |
| 1349 |
: std::optional<ImU32>{}) { |
| 1350 |
drawList->AddRectFilled(tl, {clipMax.x, tl.y + rowHeight}, *color); |
| 1351 |
} |
| 1352 |
tl.y += style.ItemSpacing.y; |
| 1353 |
switch (trace.getType()) { |
| 1354 |
case Tracer::Trace::Type::MONOSTATE: |
| 1355 |
drawEventsVoid(tl, convertor, maxT, visibleEvents, rowHovered); |
| 1356 |
break; |
| 1357 |
case Tracer::Trace::Type::BOOL: |
| 1358 |
drawEventsBool(tl, convertor, maxT, visibleEvents, rowHovered); |
| 1359 |
break; |
| 1360 |
default: |
| 1361 |
drawEventsValue(tl, convertor, maxT, visibleEvents, rowHovered, trace.getFormat()); |
| 1362 |
} |
| 1363 |
}); |
| 1364 |
auto drawLine = [&](EmuTime t, ImU32 color) { |
| 1365 |
if (t != EmuTime::infinity()) { |
| 1366 |
auto x = float(convertor.timeToX(t) + clipMin.x); |
| 1367 |
drawList->AddLine({x, clipMin.y}, {x, clipMax.y}, color); |
| 1368 |
} |
| 1369 |
}; |
| 1370 |
drawLine(selectedTime1, IM_COL32(0, 255, 0, 255)); |
| 1371 |
drawLine(selectedTime2, IM_COL32(255, 0, 0, 255)); |
| 1372 |
|
| 1373 |
if (now < maxT) { |
| 1374 |
auto nowX = convertor.timeToX(now); |
| 1375 |
auto x = nowX + clipMin.x; |
| 1376 |
drawList->AddRectFilled({x, clipMin.y}, clipMax, IM_COL32(0, 0, 0, 128)); |
| 1377 |
|
| 1378 |
if (viewStartTime <= now && now <= (viewStartTime + viewDuration)) { |
| 1379 |
if ((now - EmuTime::zero()).toDouble() >= (viewDuration.toDouble() * 0.99)) { |
| 1380 |
prevNowX = nowX; |
| 1381 |
} |
| 1382 |
} |
| 1383 |
} |
| 1384 |
|
| 1385 |
drawList->PopClipRect(); |
| 1386 |
|
| 1387 |
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { |
| 1388 |
ImGui::OpenPopup("graph-context"); |
| 1389 |
ctxMouseX = mouseX; |
| 1390 |
ctxMouseRow = mouseRow; |
| 1391 |
} |
| 1392 |
if (hovered && ImGui::IsMouseDragging(ImGuiMouseButton_Middle)) { |
| 1393 |
auto delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Middle); |
| 1394 |
ImGui::ResetMouseDragDelta(ImGuiMouseButton_Middle); |
| 1395 |
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); |
| 1396 |
if (delta.x < 0.0f) { |
| 1397 |
viewStartTime += convertor.deltaXtoDuration(-delta.x); |
| 1398 |
} else if (delta.x > 0.0f) { |
| 1399 |
viewStartTime = viewStartTime.saturateSubtract(convertor.deltaXtoDuration(delta.x)); |
| 1400 |
} |
| 1401 |
scrollChanged = true; |
| 1402 |
} |
| 1403 |
|
| 1404 |
im::Popup("graph-context", [&]{ |
| 1405 |
ImGui::TextUnformatted("Scroll to"); |
| 1406 |
im::Indent([&]{ |
| 1407 |
if (ImGui::MenuItem("Primary")) { |
| 1408 |
scrollTo(selectedTime1); |
| 1409 |
} |
| 1410 |
if (ImGui::MenuItem("Secondary")) { |
| 1411 |
scrollTo(selectedTime2); |
| 1412 |
} |
| 1413 |
ImGui::AlignTextToFramePadding(); |
| 1414 |
ImGui::TextUnformatted("Position"); |
| 1415 |
ImGui::SameLine(); |
| 1416 |
bool valid = StringOp::stringTo<double>(ctxInputPos).has_value(); |
| 1417 |
im::StyleColor(!valid, ImGuiCol_Text, getColor(imColor::ERROR), [&]{ |
| 1418 |
ImGui::SetNextItemWidth(6.0f * ImGui::GetFontSize()); |
| 1419 |
if (ImGui::InputText("##pos", &ctxInputPos, ImGuiInputTextFlags_EnterReturnsTrue)) { |
| 1420 |
if (auto d = StringOp::stringTo<double>(ctxInputPos)) { |
| 1421 |
auto unitFactor = getUnitConversionFactor(); |
| 1422 |
scrollTo(EmuTime::fromDouble(*d / unitFactor)); |
| 1423 |
ImGui::CloseCurrentPopup(); |
| 1424 |
} |
| 1425 |
} |
| 1426 |
}); |
| 1427 |
}); |
| 1428 |
ImGui::Separator(); |
| 1429 |
const auto& reverseManager = motherBoard.getReverseManager(); |
| 1430 |
bool reverseEnabled = reverseManager.isCollecting(); |
| 1431 |
im::Disabled(!reverseEnabled, [&]{ |
| 1432 |
if (ImGui::MenuItem("Reverse emulation to here")) { |
| 1433 |
auto time = convertor.xToTime(ctxMouseX); |
| 1434 |
if (const auto* trace = getTrace(traces, ctxMouseRow)) { |
| 1435 |
time = snapToEvent(time, convertor, trace->events); |
| 1436 |
} |
| 1437 |
manager.executeDelayed(makeTclList("reverse", "goto", time.toDouble())); |
| 1438 |
} |
| 1439 |
simpleToolTip(reverseEnabled |
| 1440 |
? "Rewind/forward emulation till this position in the timeline.\n" |
| 1441 |
"Note: during replay NO new trace information is collected." |
| 1442 |
: "Requires 'reverse' to be enabled. Enable via 'Save state > Reverse/replay settings > Enable reverse/replay'"); |
| 1443 |
}); |
| 1444 |
}); |
| 1445 |
}); |
| 1446 |
} |
| 1447 |
|
| 1448 |
void ImGuiTraceViewer::paint(MSXMotherBoard* motherBoard) |
| 1449 |
{ |
| 1450 |
if (!motherBoard) return; |
| 1451 |
|
| 1452 |
if (show) { |
| 1453 |
ImGui::SetNextWindowSize(gl::vec2{68, 29} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver); |
| 1454 |
int flags = showMenuBar ? ImGuiWindowFlags_MenuBar : 0; |
| 1455 |
im::Window("Probe/Trace Viewer", &show, flags, [&] { |
| 1456 |
paintMain(*motherBoard); |
| 1457 |
}); |
| 1458 |
} |
| 1459 |
if (showSelect) { |
| 1460 |
ImGui::SetNextWindowSize(gl::vec2{18, 27} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver); |
| 1461 |
im::Window("Select probes and traces", &showSelect, [&] { |
| 1462 |
paintSelect(*motherBoard); |
| 1463 |
}); |
| 1464 |
} |
| 1465 |
if (showHelp) { |
| 1466 |
ImGui::SetNextWindowSize(gl::vec2{50, 38} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver); |
| 1467 |
im::Window("Trace Viewer Help", &showHelp, [&] { |
| 1468 |
paintHelp(); |
| 1469 |
}); |
| 1470 |
} |
| 1471 |
} |
| 1472 |
|
| 1473 |
const std::vector<Tracer::Trace*>& ImGuiTraceViewer::getTraces(MSXMotherBoard& motherBoard) |
| 1474 |
{ |
| 1475 |
auto& debugger = motherBoard.getDebugger(); |
| 1476 |
auto& tracer = debugger.getTracer(); |
| 1477 |
calcTraces(tracer); |
| 1478 |
return traces; |
| 1479 |
} |
| 1480 |
|
| 1481 |
void ImGuiTraceViewer::calcTraces(Tracer& tracer) |
| 1482 |
{ |
| 1483 |
const auto& allTraces = tracer.getTraces(); |
| 1484 |
|
| 1485 |
for (const auto& trace : allTraces) { |
| 1486 |
bool isProbe = !trace->isUserTrace(); |
| 1487 |
if (!isProbe || allUserTraces) { |
| 1488 |
|
| 1489 |
if (!contains(selectedTraces, trace->name, &SelectedTrace::name)) { |
| 1490 |
selectedTraces.emplace_back(trace->name, isProbe); |
| 1491 |
} |
| 1492 |
} |
| 1493 |
} |
| 1494 |
traces.clear(); |
| 1495 |
for (const auto& sel : selectedTraces) { |
| 1496 |
if (allUserTraces || sel.selected) { |
| 1497 |
if (auto it = std::ranges::find(allTraces, sel.name, &Tracer::Trace::name); |
| 1498 |
it != allTraces.end()) { |
| 1499 |
traces.push_back(it->get()); |
| 1500 |
} |
| 1501 |
} |
| 1502 |
} |
| 1503 |
} |
| 1504 |
|
| 1505 |
void ImGuiTraceViewer::paintMain(MSXMotherBoard& motherBoard) |
| 1506 |
{ |
| 1507 |
auto& debugger = motherBoard.getDebugger(); |
| 1508 |
auto& tracer = debugger.getTracer(); |
| 1509 |
calcTraces(tracer); |
| 1510 |
|
| 1511 |
if (ImGui::Shortcut(ImGuiKey_UpArrow, ImGuiInputFlags_Repeat)) { |
| 1512 |
if (selectedRow > 0) --selectedRow; |
| 1513 |
} else if (ImGui::Shortcut(ImGuiKey_DownArrow, ImGuiInputFlags_Repeat)) { |
| 1514 |
if (selectedRow < int(traces.size() - 1)) ++selectedRow; |
| 1515 |
} |
| 1516 |
|
| 1517 |
EmuTime minT = EmuTime::infinity(); |
| 1518 |
EmuTime maxT = EmuTime::zero(); |
| 1519 |
if (timelineStart || timelineStop) { |
| 1520 |
for (const auto& trace : traces) { |
| 1521 |
if (trace->events.empty()) continue; |
| 1522 |
minT = std::min(minT, trace->events.front().time); |
| 1523 |
maxT = std::max(maxT, trace->events.back().time); |
| 1524 |
} |
| 1525 |
if (minT > maxT) minT = maxT; |
| 1526 |
} |
| 1527 |
|
| 1528 |
static constexpr auto margin = EmuDuration::sec(0.02); |
| 1529 |
auto now = motherBoard.getCurrentTime(); |
| 1530 |
minT = timelineStart ? minT : EmuTime::zero(); |
| 1531 |
minT = std::min(minT, viewStartTime); |
| 1532 |
maxT = timelineStop ? maxT : now; |
| 1533 |
maxT = std::max(maxT, viewStartTime + viewDuration); |
| 1534 |
maxT += margin; |
| 1535 |
if (maxT < minT) maxT = minT + margin; |
| 1536 |
auto totalT = maxT - minT; |
| 1537 |
|
| 1538 |
auto viewScreenWidth = ImGui::GetContentRegionAvail().x - col0_width - splitterWidth; |
| 1539 |
|
| 1540 |
if (followNow && !scrollChanged && prevNowX > 0.0f) { |
| 1541 |
|
| 1542 |
|
| 1543 |
|
| 1544 |
|
| 1545 |
auto pixelsPerTick = viewScreenWidth / float(viewDuration.toUint64()); |
| 1546 |
auto d = EmuDuration(uint64_t(prevNowX / pixelsPerTick)); |
| 1547 |
viewStartTime = now.saturateSubtract(d); |
| 1548 |
} |
| 1549 |
prevNowX = -1.0f; |
| 1550 |
scrollChanged = false; |
| 1551 |
|
| 1552 |
if (showMenuBar) { |
| 1553 |
im::MenuBar([&]{ |
| 1554 |
drawMenuBar(minT, totalT, tracer); |
| 1555 |
}); |
| 1556 |
} |
| 1557 |
drawToolBar(minT, totalT, viewScreenWidth); |
| 1558 |
ImGui::Separator(); |
| 1559 |
|
| 1560 |
const auto& io = ImGui::GetIO(); |
| 1561 |
const auto& style = ImGui::GetStyle(); |
| 1562 |
float rulerHeight = ImGui::GetTextLineHeight() * 1.5f; |
| 1563 |
auto rowHeight = ImGui::GetFrameHeight() + 2.0f * style.ItemSpacing.y; |
| 1564 |
auto mouseY = io.MousePos.y - ImGui::GetCursorScreenPos().y + scrollY - rulerHeight; |
| 1565 |
auto mouseRow = static_cast<int>(mouseY / rowHeight); |
| 1566 |
if (mouseRow < 0 || mouseRow >= int(traces.size())) mouseRow = -1; |
| 1567 |
|
| 1568 |
drawNames(rulerHeight, rowHeight, mouseRow); |
| 1569 |
drawSplitter(splitterWidth); |
| 1570 |
Convertor convertor{viewStartTime, viewDuration, viewScreenWidth}; |
| 1571 |
drawGraphs(rulerHeight, rowHeight, mouseRow, convertor, minT, maxT, now, motherBoard); |
| 1572 |
} |
| 1573 |
|
| 1574 |
void ImGuiTraceViewer::paintSelect(MSXMotherBoard& motherBoard) |
| 1575 |
{ |
| 1576 |
auto& debugger = motherBoard.getDebugger(); |
| 1577 |
auto& tracer = debugger.getTracer(); |
| 1578 |
const auto& allTraces = tracer.getTraces(); |
| 1579 |
|
| 1580 |
const auto& style = ImGui::GetStyle(); |
| 1581 |
auto rightSpace = ImGui::CalcTextSize("(?)").x + style.ItemSpacing.x; |
| 1582 |
auto boxHeight = ImGui::GetFrameHeightWithSpacing() * 4.7f; |
| 1583 |
|
| 1584 |
ImGui::SeparatorText("Probes"); |
| 1585 |
im::Child("##probes", {-rightSpace, boxHeight}, ImGuiChildFlags_Borders, [&]{ |
| 1586 |
auto& allProbes = debugger.getProbes(); |
| 1587 |
im::ListClipper(allProbes.size(), [&](int i) { |
| 1588 |
const auto& probe = *allProbes[i]; |
| 1589 |
const auto& name = probe.getName(); |
| 1590 |
bool selected = contains(allTraces, name, &Tracer::Trace::name); |
| 1591 |
if (ImGui::Checkbox(name.c_str(), &selected)) { |
| 1592 |
if (selected) { |
| 1593 |
tracer.selectProbe(debugger, name); |
| 1594 |
if (!contains(selectedTraces, name, &SelectedTrace::name)) { |
| 1595 |
selectedTraces.emplace_back(name, true); |
| 1596 |
} |
| 1597 |
} else { |
| 1598 |
tracer.unselectProbe(debugger, name); |
| 1599 |
if (auto it = std::ranges::find(selectedTraces, name, &SelectedTrace::name); |
| 1600 |
it != selectedTraces.end()) { |
| 1601 |
selectedTraces.erase(it); |
| 1602 |
} |
| 1603 |
} |
| 1604 |
} |
| 1605 |
simpleToolTip(probe.getDescription()); |
| 1606 |
}); |
| 1607 |
}); |
| 1608 |
HelpMarker( |
| 1609 |
"Probes are provided by openMSX (builtin). " |
| 1610 |
"The available probes depend on the selected MSX machine and extensions.\n" |
| 1611 |
"Some probes generate A LOT of data, therefor there's no option to automatically enable all."); |
| 1612 |
|
| 1613 |
ImGui::SeparatorText("User traces"); |
| 1614 |
if (ImGui::RadioButton("Auto select all", allUserTraces)) { |
| 1615 |
allUserTraces = true; |
| 1616 |
} |
| 1617 |
HelpMarker("Automatically select all user defined traces. Including when more traces are created later."); |
| 1618 |
if (ImGui::RadioButton("Manual select", !allUserTraces)) { |
| 1619 |
allUserTraces = false; |
| 1620 |
} |
| 1621 |
im::DisabledIndent(allUserTraces, [&]{ |
| 1622 |
im::Child("##traces", {-rightSpace, boxHeight}, ImGuiChildFlags_Borders, [&]{ |
| 1623 |
std::vector<zstring_view> userTraceNames; |
| 1624 |
userTraceNames.reserve(allTraces.size()); |
| 1625 |
for (const auto& trace : allTraces) { |
| 1626 |
if (trace->isUserTrace()) { |
| 1627 |
userTraceNames.emplace_back(trace->name); |
| 1628 |
} |
| 1629 |
} |
| 1630 |
im::ListClipper(userTraceNames.size(), [&](int i) { |
| 1631 |
const auto& name = userTraceNames[i]; |
| 1632 |
auto it1 = std::ranges::find(selectedTraces, name, &SelectedTrace::name); |
| 1633 |
bool selected = it1 != selectedTraces.end() && it1->selected; |
| 1634 |
if (ImGui::Checkbox(name.c_str(), &selected)) { |
| 1635 |
if (selected) { |
| 1636 |
if (auto it = std::ranges::find(selectedTraces, name, &SelectedTrace::name); |
| 1637 |
it != selectedTraces.end()) { |
| 1638 |
it->selected = true; |
| 1639 |
} else { |
| 1640 |
selectedTraces.emplace_back(std::string(name), true); |
| 1641 |
} |
| 1642 |
} else { |
| 1643 |
if (auto it = std::ranges::find(selectedTraces, name, &SelectedTrace::name); |
| 1644 |
it != selectedTraces.end()) { |
| 1645 |
it->selected = false; |
| 1646 |
} |
| 1647 |
} |
| 1648 |
} |
| 1649 |
simpleToolTip([&]{ |
| 1650 |
auto* trace = tracer.findTrace(name); assert(trace); |
| 1651 |
return trace->description; |
| 1652 |
}); |
| 1653 |
}); |
| 1654 |
}); |
| 1655 |
HelpMarker( |
| 1656 |
"Traces are created by the user, via the 'debug trace add' command. " |
| 1657 |
"Typically trace data is added via breakpoints or watchpoints.\n" |
| 1658 |
"For example in the breakpoint editor set a breakpoint on your function, " |
| 1659 |
"and then in the action column (possibly unhide via right-click the column header) " |
| 1660 |
"enter the command 'debug trace add myFunction [reg A]'. " |
| 1661 |
"This will add a trace point whenever your function is called, " |
| 1662 |
"and it will log the value of register A"); |
| 1663 |
}); |
| 1664 |
} |
| 1665 |
|
| 1666 |
static inline void RenderParagraph(std::string_view text) |
| 1667 |
{ |
| 1668 |
im::TextWrapPos([&]{ |
| 1669 |
ImGui::TextUnformatted(text); |
| 1670 |
}); |
| 1671 |
} |
| 1672 |
|
| 1673 |
static inline void RenderBullet(std::string_view text) |
| 1674 |
{ |
| 1675 |
ImGui::Bullet(); |
| 1676 |
ImGui::SameLine(); |
| 1677 |
RenderParagraph(text); |
| 1678 |
} |
| 1679 |
|
| 1680 |
static inline void TextUnformattedUnderlined(std::string_view text) |
| 1681 |
{ |
| 1682 |
ImGui::TextUnformatted(text); |
| 1683 |
auto size = ImGui::CalcTextSize(text); |
| 1684 |
auto p = ImGui::GetItemRectMin(); |
| 1685 |
auto start = gl::vec2{p.x, p.y + size.y + 2.0f}; |
| 1686 |
auto end = gl::vec2{p.x + size.x, start.y}; |
| 1687 |
auto color = getColor(imColor::TEXT); |
| 1688 |
ImGui::GetWindowDrawList()->AddLine(start, end, color); |
| 1689 |
ImGui::NewLine(); |
| 1690 |
} |
| 1691 |
|
| 1692 |
void ImGuiTraceViewer::paintHelp() |
| 1693 |
{ |
| 1694 |
if (helpSection == HelpSection::OVERVIEW) { |
| 1695 |
ImGui::SetScrollHereY(0.0f); |
| 1696 |
} |
| 1697 |
TextUnformattedUnderlined("Overview"); |
| 1698 |
RenderParagraph( |
| 1699 |
"The 'Probe/Trace Viewer' is an openMSX debugging tool to visualize events " |
| 1700 |
"(and optionally associated data) on a timeline. This can help answer " |
| 1701 |
"questions like:"); |
| 1702 |
RenderBullet( |
| 1703 |
"How much time passed between the VDP generating a VBLANK IRQ and the Z80 responding?"); |
| 1704 |
RenderBullet( |
| 1705 |
"When is the VDP busy executing commands? Can I change my code to increase " |
| 1706 |
"utilization (less idle time)?"); |
| 1707 |
RenderBullet( |
| 1708 |
"When is my function called and what parameters were passed?"); |
| 1709 |
|
| 1710 |
RenderParagraph( |
| 1711 |
"First, you need to tell openMSX what information to collect. This can be in the " |
| 1712 |
"form of builtin openMSX 'probes' or user-defined 'traces'. See the section " |
| 1713 |
"'Probes and traces' for more details. In some cases collecting this information " |
| 1714 |
"can be expensive. That's why openMSX does not collect everything by default " |
| 1715 |
"(which would also clutter the view)."); |
| 1716 |
RenderParagraph( |
| 1717 |
"Next, run your test case. During this process you'll see the graphs being " |
| 1718 |
"created. Typically, after some interesting data has been collected you'll " |
| 1719 |
"want to pause emulation and examine the graph in detail. See the 'Graphs' " |
| 1720 |
"section."); |
| 1721 |
RenderParagraph( |
| 1722 |
"The main purpose of this tool is to analyze the collected data. Often this " |
| 1723 |
"involves cross-correlating multiple data sources. For example, how many " |
| 1724 |
"cycles are there between entering 'myFuncA' and starting the next VDP " |
| 1725 |
"command? When was the last IRQ before 'myFuncB'? See the 'Navigation' " |
| 1726 |
"section."); |
| 1727 |
RenderParagraph( |
| 1728 |
"If this is still a bit abstract, you can read the 'Example' section for a " |
| 1729 |
"more concrete example."); |
| 1730 |
ImGui::NewLine(); |
| 1731 |
ImGui::NewLine(); |
| 1732 |
|
| 1733 |
if (helpSection == HelpSection::PROBES_AND_TRACES) { |
| 1734 |
ImGui::SetScrollHereY(0.0f); |
| 1735 |
} |
| 1736 |
TextUnformattedUnderlined("Probes and Traces"); |
| 1737 |
RenderParagraph( |
| 1738 |
"As mentioned in the 'Overview' section, the first step is to select the " |
| 1739 |
"information sources. This is done via the 'Select probes and traces' window. " |
| 1740 |
"That window can be opened via 'menu bar > File > Select probes and traces', " |
| 1741 |
"or by clicking the 'Select...' button in the left column."); |
| 1742 |
RenderParagraph( |
| 1743 |
"This window has two main parts: 'Probes' and 'User traces'. The 'Probes' part " |
| 1744 |
"lists builtin openMSX probes with checkboxes to enable collection. The 'User " |
| 1745 |
"traces' part offers 'Auto select all' or 'Manual select'. 'Manual select' shows " |
| 1746 |
"currently defined user traces with checkboxes. 'Auto select all' selects all " |
| 1747 |
"user traces, including ones not yet created (and thus not yet visible in " |
| 1748 |
"the list)."); |
| 1749 |
RenderParagraph( |
| 1750 |
"Both probes and traces generate timestamped events. Some events only record a " |
| 1751 |
"timestamp (for example 'z80.accept.IRQ'). Other events carry extra information: " |
| 1752 |
"'VDP.commandExecuting' also carries a boolean indicating whether a command " |
| 1753 |
"started or stopped. User traces may carry arbitrary additional data (boolean, " |
| 1754 |
"integer, float, string), for example function parameters."); |
| 1755 |
RenderParagraph( |
| 1756 |
"Probes are builtin low-level events provided by openMSX; traces are " |
| 1757 |
"user-defined and typically project-specific. Because some builtin probes " |
| 1758 |
"generate many events or are expensive to collect, enable only those you " |
| 1759 |
"need; traces are usually smaller and tailored to your debugging needs."); |
| 1760 |
RenderParagraph( |
| 1761 |
"openMSX currently provides generic probes for IRQs and the VDP command " |
| 1762 |
"engine, providing low-level information not otherwise exposed via " |
| 1763 |
"debuggables. Future openMSX versions may add more builtin probes."); |
| 1764 |
RenderParagraph( |
| 1765 |
"Traces are user-defined and often attached to breakpoints or watchpoints. " |
| 1766 |
"Create a trace data point with the Tcl command:"); |
| 1767 |
im::Font(manager.fontMono, [&]{ im::Indent([&]{ RenderParagraph( |
| 1768 |
"debug trace add <trace-name> [<value>]"); }); }); |
| 1769 |
RenderParagraph( |
| 1770 |
"Execute 'help debug trace' and 'help debug trace add' in the console for " |
| 1771 |
"details. Here's a full example, attach to a breakpoint and log register A as data:"); |
| 1772 |
im::Font(manager.fontMono, [&]{ im::Indent([&]{ RenderParagraph( |
| 1773 |
"debug breakpoint create -address 0x1234 -command {debug trace add " |
| 1774 |
"myFunction [reg A]}"); }); }); |
| 1775 |
RenderParagraph( |
| 1776 |
"This logs a data point for the trace named 'myFunction' whenever the breakpoint at address 0x1234 " |
| 1777 |
"triggers. Using 'debug trace add ...' as the breakpoint '-command' records the trace " |
| 1778 |
"instead of stopping emulation. The '[reg A]' expression logs the current value " |
| 1779 |
"of the Z80 register A."); |
| 1780 |
RenderParagraph( |
| 1781 |
"You can create the breakpoint via the above Tcl command or via the GUI " |
| 1782 |
"(Debugger > Breakpoints > Editor). Enter the action in the 'Action' column " |
| 1783 |
"(unhide that column from the table header context menu if needed)."); |
| 1784 |
ImGui::NewLine(); |
| 1785 |
ImGui::NewLine(); |
| 1786 |
|
| 1787 |
if (helpSection == HelpSection::GRAPHS) { |
| 1788 |
ImGui::SetScrollHereY(0.0f); |
| 1789 |
} |
| 1790 |
TextUnformattedUnderlined("Graphs"); |
| 1791 |
RenderParagraph( |
| 1792 |
"Once probes/traces are selected and data has been collected (by running " |
| 1793 |
"a test case), the results appear in the main area of the Probe/Trace " |
| 1794 |
"Viewer."); |
| 1795 |
RenderParagraph( |
| 1796 |
"The left column lists probe/trace names, the right area shows the " |
| 1797 |
"timeline and the recorded data."); |
| 1798 |
RenderParagraph( |
| 1799 |
"There are three data types, each rendered differently:"); |
| 1800 |
RenderBullet( |
| 1801 |
"Event-only: timestamped events without extra data, shown as small up-arrows."); |
| 1802 |
RenderBullet( |
| 1803 |
"Boolean: timestamped 0/1 transitions, drawn as a square wave (edges at transitions)."); |
| 1804 |
RenderBullet( |
| 1805 |
"Value traces: events carrying integers, floats, or strings; shown as " |
| 1806 |
"elongated hexagons that span until the next value, with the value text " |
| 1807 |
"inside."); |
| 1808 |
|
| 1809 |
RenderParagraph( |
| 1810 |
"By default the graph order follows the selection order, but drag names in the " |
| 1811 |
"left column to reorder."); |
| 1812 |
RenderParagraph( |
| 1813 |
"If the collected information does not fit on screen, zoom or scroll to inspect it:"); |
| 1814 |
RenderBullet( |
| 1815 |
"Zoom: via 'View > Zoom', toolbar icons, or shortcuts (default: Ctrl-=, Ctrl--, Ctrl-0)."); |
| 1816 |
RenderBullet( |
| 1817 |
"Scroll: via bottom scrollbar or Left/Right cursor keys (use Shift + Left/Right for faster movement)."); |
| 1818 |
|
| 1819 |
RenderParagraph("Markers:"); |
| 1820 |
RenderBullet("Primary marker (green): left-click near an edge to place or snap it."); |
| 1821 |
RenderBullet("Secondary marker (red): shift-left-click near an edge to place or snap it."); |
| 1822 |
|
| 1823 |
RenderParagraph( |
| 1824 |
"Near the top are two text fields showing the marker positions, edit " |
| 1825 |
"them to move the markers. A third read-only field shows the distance " |
| 1826 |
"between the markers. Use the units dropdown to choose seconds or Z80 " |
| 1827 |
"clock cycles."); |
| 1828 |
RenderParagraph( |
| 1829 |
"Use 'View > Timeline > start/stop' to show the full run (from emulation start to current time) " |
| 1830 |
"or only the interval containing collected events. Showing only the " |
| 1831 |
"interval with events is useful when debugging a short section of a " |
| 1832 |
"long-running program."); |
| 1833 |
RenderParagraph( |
| 1834 |
"Use 'View > Timeline > Origin' to place the origin at start of emulation (this gives " |
| 1835 |
"stable time points) or at the primary marker (this gives shorter time values)."); |
| 1836 |
ImGui::NewLine(); |
| 1837 |
ImGui::NewLine(); |
| 1838 |
|
| 1839 |
if (helpSection == HelpSection::NAVIGATION) { |
| 1840 |
ImGui::SetScrollHereY(0.0f); |
| 1841 |
} |
| 1842 |
TextUnformattedUnderlined("Navigation"); |
| 1843 |
RenderParagraph( |
| 1844 |
"An important feature of the 'Probe/Trace Viewer' is measuring and relating " |
| 1845 |
"events across traces (for example, measuring distances or the ordering " |
| 1846 |
"between events). The UI offers the following helpers."); |
| 1847 |
RenderParagraph( |
| 1848 |
"(Shift-)left-click near an edge snaps the primary (or secondary) " |
| 1849 |
"marker to that edge, allowing precise distance measurements between " |
| 1850 |
"events."); |
| 1851 |
RenderParagraph("Navigation functions (six total):"); |
| 1852 |
RenderBullet("Go to next/previous (any) edge"); |
| 1853 |
RenderBullet("Go to next/previous positive (=rising) edge"); |
| 1854 |
RenderBullet("Go to next/previous negative (=falling) edge"); |
| 1855 |
|
| 1856 |
RenderParagraph( |
| 1857 |
"Positive/negative are only relevant for boolean traces: a positive edge is " |
| 1858 |
"a transition from low (0) to high (1); a negative edge is the opposite. " |
| 1859 |
"For example, in VDP.commandExecuting a positive edge marks when a " |
| 1860 |
"command starts and the negative edge when it finishes."); |
| 1861 |
RenderParagraph( |
| 1862 |
"These functions are available as six toolbar icons. Left-click an icon to " |
| 1863 |
"move the primary marker; shift-left-click to move the secondary marker. " |
| 1864 |
"Keyboard shortcuts are available (default: Ctrl+A,S,D,F,G,H, six " |
| 1865 |
"consecutive keys, same order as the toolbar icons). Ctrl alone moves " |
| 1866 |
"the primary marker; Ctrl+Shift moves the secondary marker."); |
| 1867 |
RenderParagraph( |
| 1868 |
"You can relate events across graphs using the navigation functions. For " |
| 1869 |
"example, locate a rising edge in 'VDP.IRQvertical' (when the VDP asserts the " |
| 1870 |
"VBLANK IRQ). Select the 'z80.acceptIRQ' row without moving the marker (click " |
| 1871 |
"the row name or use the up/down cursor keys), then go to the next event to " |
| 1872 |
"see when the Z80 accepted the interrupt (the Z80 will jump to the ISR, " |
| 1873 |
"typically at address 0x0038). Selecting 'VDP.IRQvertical' again and advancing to " |
| 1874 |
"the next edge shows when the ISR cleared the VDP IRQ."); |
| 1875 |
ImGui::NewLine(); |
| 1876 |
ImGui::NewLine(); |
| 1877 |
|
| 1878 |
if (helpSection == HelpSection::EXAMPLE) { |
| 1879 |
ImGui::SetScrollHereY(0.0f); |
| 1880 |
} |
| 1881 |
TextUnformattedUnderlined("Example"); |
| 1882 |
RenderParagraph( |
| 1883 |
"Hopefully the other help sections have made the purpose of the 'Probe/Trace " |
| 1884 |
"Viewer' clear. But if it's still too abstract then here's an example. To " |
| 1885 |
"avoid dependencies on external MSX software, we'll analyze the MSX-BASIC " |
| 1886 |
"startup sequence."); |
| 1887 |
RenderParagraph( |
| 1888 |
"First we need to do some preparation:"); |
| 1889 |
RenderBullet( |
| 1890 |
"Start a Philips NMS 8255 machine (other machines work too but may need slight adjustments)."); |
| 1891 |
RenderBullet( |
| 1892 |
"Pause emulation quickly (before or during the boot logo) so you have " |
| 1893 |
"time to set up probes and traces."); |
| 1894 |
RenderParagraph( |
| 1895 |
"Open the openMSX console (press F10 or 'Tools > Show console') and enter:"); |
| 1896 |
im::Font(manager.fontMono, [&]{ im::Indent([&]{ RenderParagraph( |
| 1897 |
"debug breakpoint create -address 0x0919 -command {debug trace add chput " |
| 1898 |
"[format \"%c (%d)\" [reg a] [reg a]]}"); }); }); |
| 1899 |
RenderParagraph( |
| 1900 |
"Alternatively, create the breakpoint via 'Debugger > Breakpoints > Editor': " |
| 1901 |
"click 'Add', enter '0x0919' in the 'Address' column, and put the 'debug trace " |
| 1902 |
"add ...' command in the 'Action' column (unhide that column if needed). 0x0919 " |
| 1903 |
"is the CHPUT BIOS function entry used by MSX-BASIC (MSX-BASIC calls this " |
| 1904 |
"address directly rather than the stable entry point at 0x00A2). The 'debug " |
| 1905 |
"trace add' command records a data point for the user-defined 'chput' trace; " |
| 1906 |
"the 'format' expression prints register 'A' both as a character and as a " |
| 1907 |
"decimal integer."); |
| 1908 |
RenderParagraph( |
| 1909 |
"Open the Probe/Trace Viewer (Debugger > Probe/Trace viewer) and click " |
| 1910 |
"'Select...' in the left column (or use 'File > Select probes and traces')."); |
| 1911 |
RenderParagraph( |
| 1912 |
"In the selector enable 'VDP.IRQvertical' and 'z80.acceptIRQ' (and any other " |
| 1913 |
"probes you're interested in). In the 'User traces' part, enable 'Auto select all' so your " |
| 1914 |
"'chput' trace will appear once it is recorded."); |
| 1915 |
RenderParagraph( |
| 1916 |
"Now resume emulation and wait for MSX-BASIC to finish booting, then pause again."); |
| 1917 |
RenderParagraph("Inspecting the data:"); |
| 1918 |
RenderBullet( |
| 1919 |
"Use Zoom to fit ('toolbar > magnifier' or 'Ctrl-0') for an overview."); |
| 1920 |
RenderBullet( |
| 1921 |
"Zoom into the dense region using the toolbar or by hovering the mouse " |
| 1922 |
"over the region and using Ctrl + mouse wheel."); |
| 1923 |
RenderBullet( |
| 1924 |
"At sufficient zoom you'll see individual printed characters and their values (e.g. M (77))."); |
| 1925 |
RenderBullet( |
| 1926 |
"Place the primary marker at the start of the 'M' event (left-click near " |
| 1927 |
"the edge); place the secondary marker at the end of the 'M' event " |
| 1928 |
"(shift-left-click)."); |
| 1929 |
RenderParagraph( |
| 1930 |
"The dist field in the top bar shows the time between markers. In this " |
| 1931 |
"case printing 'M' took 0.00043 seconds; switching units to Z80 cycles shows " |
| 1932 |
"1553 cycles; CHPUT is relatively slow."); |
| 1933 |
RenderParagraph( |
| 1934 |
"At 9.92 seconds printing 'o' took much longer (about 8458 cycles). The " |
| 1935 |
"z80.acceptIRQ row shows an interrupt at 9.921 seconds; the ISR consumed " |
| 1936 |
"roughly 6900 cycles, explaining the longer duration."); |
| 1937 |
RenderParagraph( |
| 1938 |
"This simple example demonstrates how visualizing and correlating traces can " |
| 1939 |
"reveal timing and interaction details between subsystems."); |
| 1940 |
|
| 1941 |
helpSection = {}; |
| 1942 |
} |
| 1943 |
|
| 1944 |
} |