Modules/Graphics/private/InputRecording.cpp
Шурупов Илья Викторович 37a650b1e8 Add frame profiling, GPU timers and input recording to the libraries
Profiler (Modules) collects per-frame wall and thread CPU time, named
inclusive sections, counters and GPU durations. It is off by default, so
a disabled scope costs one branch. Application, WidgetApplication,
VulkanContext and Canvas mark the frame phases (process, dispatch,
advance, draw, paint, UI flush, acquire, submit, present).

GpuTimer wraps Vulkan timestamp queries in a small ring that is read
without stalling; the window frame and any RenderBuffer that asks for it
report GPU time into the profiler.

InputRecording stores raw window input with exact timing, pen pressure
and tilt as a line-based text file, and InputRecorder captures it
through a new raw tap in the InputCoalescer that sees every platform
event before coalescing.
2026-10-15 20:03:20 +03:00

248 lines
7.3 KiB
C++

#include "InputRecording.hpp"
#include <chrono>
#include <cstdio>
#include <fstream>
#include <sstream>
namespace tp {
namespace {
const char* const kKindNames[] = { "sample", "move", "leave", "button", "scroll", "key", "focus" };
std::optional<RecordedInput::Kind> kindNamed(const std::string& name) {
for (size_t index = 0; index < std::size(kKindNames); index++) {
if (name == kKindNames[index]) return (RecordedInput::Kind) index;
}
return std::nullopt;
}
double steadyMs() {
const auto now = std::chrono::steady_clock::now().time_since_epoch();
return std::chrono::duration<double, std::milli>(now).count();
}
bool isPointerKind(RecordedInput::Kind kind) {
return kind == RecordedInput::Kind::Sample || kind == RecordedInput::Kind::Move ||
kind == RecordedInput::Kind::Button || kind == RecordedInput::Kind::Leave;
}
}
void RecordedInput::deliverTo(WindowInputObserver& observer) const {
switch (kind) {
case Kind::Sample: observer.onPointerSample(sample); break;
case Kind::Move: observer.onPointerMoved(vector); break;
case Kind::Leave: observer.onPointerLeft(); break;
case Kind::Button: observer.onPointerButton(id, flag); break;
case Kind::Scroll: observer.onScrolled(vector); break;
case Kind::Key: observer.onKey(id, flag); break;
case Kind::Focus: observer.onKeyboardFocus(flag); break;
}
}
std::string InputRecording::serialize() const {
std::string text = std::string(kHeader) + "\n";
char line[256];
std::snprintf(line, sizeof(line), "window %.1f %.1f\n", windowSize.x, windowSize.y);
text += line;
for (const RecordedInput& event : events) {
const char* kind = kKindNames[(size_t) event.kind];
switch (event.kind) {
case RecordedInput::Kind::Sample:
std::snprintf(
line,
sizeof(line),
"%.3f %s %d %.5f %.4f %.4f %.4f %.4f %u\n",
event.timeMs,
kind,
(int) event.sample.tool,
event.sample.pressure,
event.sample.tilt.x,
event.sample.tilt.y,
event.sample.rotation,
event.sample.distance,
event.sample.timeMs
);
break;
case RecordedInput::Kind::Move:
case RecordedInput::Kind::Scroll:
std::snprintf(line, sizeof(line), "%.3f %s %.3f %.3f\n", event.timeMs, kind, event.vector.x, event.vector.y);
break;
case RecordedInput::Kind::Button:
case RecordedInput::Kind::Key:
std::snprintf(
line, sizeof(line), "%.3f %s %llu %d\n", event.timeMs, kind, (unsigned long long) event.id, (int) event.flag
);
break;
case RecordedInput::Kind::Focus:
std::snprintf(line, sizeof(line), "%.3f %s %d\n", event.timeMs, kind, (int) event.flag);
break;
case RecordedInput::Kind::Leave: std::snprintf(line, sizeof(line), "%.3f %s\n", event.timeMs, kind); break;
}
text += line;
}
return text;
}
std::optional<InputRecording> InputRecording::parse(const std::string& text) {
std::istringstream input(text);
std::string line;
if (!std::getline(input, line) || line != kHeader) return std::nullopt;
InputRecording recording;
while (std::getline(input, line)) {
if (line.empty()) continue;
std::istringstream fields(line);
std::string first;
fields >> first;
if (first == "window") {
fields >> recording.windowSize.x >> recording.windowSize.y;
continue;
}
RecordedInput event;
std::string kindName;
event.timeMs = std::stod(first);
fields >> kindName;
const std::optional<RecordedInput::Kind> kind = kindNamed(kindName);
if (!kind) return std::nullopt;
event.kind = *kind;
unsigned long long id = 0;
int flag = 0;
int tool = 0;
switch (event.kind) {
case RecordedInput::Kind::Sample:
fields >> tool >> event.sample.pressure >> event.sample.tilt.x >> event.sample.tilt.y >>
event.sample.rotation >> event.sample.distance >> event.sample.timeMs;
event.sample.tool = (PointerTool) tool;
break;
case RecordedInput::Kind::Move:
case RecordedInput::Kind::Scroll: fields >> event.vector.x >> event.vector.y; break;
case RecordedInput::Kind::Button:
case RecordedInput::Kind::Key:
fields >> id >> flag;
event.id = (InputID) id;
event.flag = flag != 0;
break;
case RecordedInput::Kind::Focus:
fields >> flag;
event.flag = flag != 0;
break;
case RecordedInput::Kind::Leave: break;
}
if (fields.fail()) return std::nullopt;
recording.events.push_back(event);
}
return recording;
}
bool InputRecording::save(const std::string& path) const {
std::ofstream file(path);
if (!file) return false;
file << serialize();
return (bool) file;
}
std::optional<InputRecording> InputRecording::load(const std::string& path) {
std::ifstream file(path);
if (!file) return std::nullopt;
std::stringstream text;
text << file.rdbuf();
return parse(text.str());
}
InputRecording InputRecording::translated(Vec2F offset) const {
InputRecording moved = *this;
for (RecordedInput& event : moved.events) {
if (event.kind == RecordedInput::Kind::Move) event.vector += offset;
}
return moved;
}
InputRecording InputRecording::slicedToPointerActivity() const {
InputRecording sliced;
sliced.windowSize = windowSize;
size_t first = events.size();
size_t last = 0;
for (size_t index = 0; index < events.size(); index++) {
if (events[index].kind != RecordedInput::Kind::Button) continue;
first = std::min(first, index);
last = index;
}
if (first >= events.size()) return sliced;
while (first > 0 && isPointerKind(events[first - 1].kind) && events[first - 1].kind != RecordedInput::Kind::Button)
first--;
const double origin = events[first].timeMs;
for (size_t index = first; index <= last; index++) {
if (!isPointerKind(events[index].kind)) continue;
RecordedInput event = events[index];
event.timeMs -= origin;
sliced.events.push_back(event);
}
return sliced;
}
InputRecorder::InputRecorder(Clock clock) :
mClock(clock ? std::move(clock) : Clock(steadyMs)) {}
void InputRecorder::record(RecordedInput event) {
const double now = mClock();
if (mStartMs < 0) mStartMs = now;
event.timeMs = now - mStartMs;
mRecording.events.push_back(event);
}
void InputRecorder::onPointerSample(const PointerSample& sample) {
RecordedInput event;
event.kind = RecordedInput::Kind::Sample;
event.sample = sample;
record(event);
}
void InputRecorder::onPointerMoved(Vec2F contentPosition) {
RecordedInput event;
event.kind = RecordedInput::Kind::Move;
event.vector = contentPosition;
record(event);
}
void InputRecorder::onPointerLeft() {
RecordedInput event;
event.kind = RecordedInput::Kind::Leave;
record(event);
}
void InputRecorder::onPointerButton(InputID button, bool pressed) {
RecordedInput event;
event.kind = RecordedInput::Kind::Button;
event.id = button;
event.flag = pressed;
record(event);
}
void InputRecorder::onScrolled(Vec2F delta) {
RecordedInput event;
event.kind = RecordedInput::Kind::Scroll;
event.vector = delta;
record(event);
}
void InputRecorder::onKey(InputID key, bool pressed) {
RecordedInput event;
event.kind = RecordedInput::Kind::Key;
event.id = key;
event.flag = pressed;
record(event);
}
void InputRecorder::onKeyboardFocus(bool focused) {
RecordedInput event;
event.kind = RecordedInput::Kind::Focus;
event.flag = focused;
record(event);
}
}