Skip to content

Commit b939d65

Browse files
committed
added trigger IDs
1 parent 8da6d68 commit b939d65

File tree

6 files changed

+219
-7
lines changed

6 files changed

+219
-7
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
run: |
3838
rm -rf libserum
3939
git clone https://github.com/PPUC/libserum.git libserum
40-
(cd libserum && git checkout db53827570a39e96567701fdddc3044eadb1e8d7)
40+
(cd libserum && git checkout f871ab8923460108776595170d0bef8f7a3bc8e4)
4141
4242
- if: matrix.platform == 'linux'
4343
name: Install dependencies (linux)

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
- WYSIWYG: render canvas + preview using libserum logic.
4141
- Avoid data duplication for large projects.
4242
- Preview row only renders the visible subset of frames.
43+
- libserum is the source of truth; the editor must not render differently.
4344
- Every feature addition, modification, or removal must be reflected in `handbook.md`.
4445
- libserum is the source of truth; the editor must not render differently.
4546

@@ -58,3 +59,8 @@
5859
## Open integration task
5960
- Add libserum editor API (non-owning view) to render frames + sprite matches.
6061
- Keep libserum free of new dependencies (no Qt/OpenCV).
62+
63+
## IndexedImageStore safety
64+
- `IndexedImageStore::at()` returns a raw pointer to a cached `cv::Mat` that can be evicted on subsequent cache activity.
65+
- When building UI previews or iterating through many items, use `loadCopy()` (or copy immediately) instead of holding a raw pointer.
66+
- Avoid storing `const cv::Mat*` across calls that can trigger cache eviction (`at()`, `add/remove`, cache limit changes).

app/MainWindow.cpp

Lines changed: 205 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ constexpr int kDefaultFrameHeight = 32;
118118
constexpr int kDefaultSpriteWidth = 64;
119119
constexpr int kDefaultSpriteHeight = 64;
120120
constexpr int kPreviewIconWidth = 160;
121+
constexpr int kMonochromeTriggerId = 65432;
121122
constexpr int kPreviewIconHeight = 120;
122123
constexpr int kPreviewItemWidth = 180;
123124
constexpr int kPreviewItemHeight = 150;
@@ -1179,6 +1180,7 @@ MainWindow::MainWindow(QWidget* parent)
11791180
m_frameStore->clear();
11801181
m_spriteStore->clear();
11811182
m_frameDurations.clear();
1183+
m_frameTriggerIds.clear();
11821184
m_spriteNames.clear();
11831185
m_spriteColored.clear();
11841186
m_spriteColoredX.clear();
@@ -1296,6 +1298,10 @@ MainWindow::MainWindow(QWidget* parent)
12961298
m_serumData.nframes = static_cast<uint32_t>(m_frameStore->count());
12971299
}
12981300
m_state->addFrame();
1301+
m_frameTriggerIds.push_back(0xffffffffu);
1302+
if (m_hasLegacyRoundTrip) {
1303+
m_legacyRoundTrip.trigger_ids = m_frameTriggerIds;
1304+
}
12991305
ensureUndoStacksSize();
13001306
if (m_framesList->count() > 0) {
13011307
m_framesList->setCurrentRow(m_framesList->count() - 1);
@@ -1400,6 +1406,12 @@ MainWindow::MainWindow(QWidget* parent)
14001406
if (row >= 0 && row < static_cast<int>(m_frameDynamicMaskMapsX.size())) {
14011407
m_frameDynamicMaskMapsX.erase(m_frameDynamicMaskMapsX.begin() + row);
14021408
}
1409+
if (row >= 0 && row < static_cast<int>(m_frameTriggerIds.size())) {
1410+
m_frameTriggerIds.erase(m_frameTriggerIds.begin() + row);
1411+
if (m_hasLegacyRoundTrip) {
1412+
m_legacyRoundTrip.trigger_ids = m_frameTriggerIds;
1413+
}
1414+
}
14031415
ensureUndoStacksSize();
14041416
} else if (m_spritesList->hasFocus()) {
14051417
const int row = m_spritesList->currentRow();
@@ -2370,6 +2382,14 @@ MainWindow::MainWindow(QWidget* parent)
23702382
m_frameBackgroundAssign = new QComboBox(inspectorWidget);
23712383
m_backgroundAssignLabel = new QLabel("Background", inspectorWidget);
23722384
m_shapeCompToggle = new QCheckBox("Shape comparison", inspectorWidget);
2385+
m_triggerIdSpin = new QSpinBox(inspectorWidget);
2386+
m_triggerIdSpin->setMinimum(-1);
2387+
m_triggerIdSpin->setMaximum(kMonochromeTriggerId);
2388+
m_triggerIdSpin->setSpecialValueText("None");
2389+
m_triggerIdSpin->setKeyboardTracking(false);
2390+
m_triggerIdSpin->setEnabled(false);
2391+
m_triggerMonochromeCheck = new QCheckBox("Switch to Monochrome", inspectorWidget);
2392+
m_triggerMonochromeCheck->setEnabled(false);
23732393
m_hdSourceCombo = new QComboBox(inspectorWidget);
23742394
m_hdScaleCombo = new QComboBox(inspectorWidget);
23752395
m_hdCreateButton = new QPushButton("Create HD", inspectorWidget);
@@ -2385,6 +2405,8 @@ MainWindow::MainWindow(QWidget* parent)
23852405
inspectorLayout->addRow("Counts", m_countsLabel);
23862406
inspectorLayout->addRow("Selection", m_selectionLabel);
23872407
inspectorLayout->addRow("Frame info", m_frameMetaLabel);
2408+
inspectorLayout->addRow("Trigger ID", m_triggerIdSpin);
2409+
inspectorLayout->addRow(m_triggerMonochromeCheck);
23882410
inspectorLayout->addRow("Mask", m_frameMaskAssign);
23892411
inspectorLayout->addRow("Dynamic mask", m_frameDynamicMaskAssign);
23902412
inspectorLayout->addRow("Dynamic copy", m_frameDynamicCopyButton);
@@ -2590,6 +2612,73 @@ MainWindow::MainWindow(QWidget* parent)
25902612
m_frameShapeCompModes[static_cast<std::size_t>(row)] = enabled ? 1 : 0;
25912613
}
25922614
});
2615+
connect(m_triggerMonochromeCheck, &QCheckBox::toggled, this, [this](bool enabled) {
2616+
if (!m_triggerIdSpin) {
2617+
return;
2618+
}
2619+
const int frameCount = m_frameStore ? m_frameStore->count() : 0;
2620+
if (frameCount <= 0) {
2621+
return;
2622+
}
2623+
if (m_frameTriggerIds.size() < static_cast<std::size_t>(frameCount)) {
2624+
m_frameTriggerIds.resize(static_cast<std::size_t>(frameCount), 0xffffffffu);
2625+
}
2626+
const uint32_t triggerValue = enabled ? static_cast<uint32_t>(kMonochromeTriggerId) : 0xffffffffu;
2627+
const std::vector<int> targets = targetFrameIndices();
2628+
if (targets.empty()) {
2629+
return;
2630+
}
2631+
for (int row : targets) {
2632+
if (row < 0 || row >= static_cast<int>(m_frameTriggerIds.size())) {
2633+
continue;
2634+
}
2635+
m_frameTriggerIds[static_cast<std::size_t>(row)] = triggerValue;
2636+
}
2637+
if (m_hasLegacyRoundTrip) {
2638+
m_legacyRoundTrip.trigger_ids = m_frameTriggerIds;
2639+
}
2640+
QSignalBlocker blockSpin(m_triggerIdSpin);
2641+
if (enabled) {
2642+
m_triggerIdSpin->setValue(kMonochromeTriggerId);
2643+
m_triggerIdSpin->setReadOnly(true);
2644+
} else {
2645+
m_triggerIdSpin->setReadOnly(false);
2646+
if (m_triggerIdSpin->value() >= kMonochromeTriggerId) {
2647+
m_triggerIdSpin->setValue(-1);
2648+
}
2649+
}
2650+
});
2651+
connect(m_triggerIdSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int value) {
2652+
if (!m_triggerMonochromeCheck || m_triggerMonochromeCheck->isChecked()) {
2653+
return;
2654+
}
2655+
if (value >= kMonochromeTriggerId) {
2656+
QSignalBlocker blockSpin(m_triggerIdSpin);
2657+
m_triggerIdSpin->setValue(kMonochromeTriggerId - 1);
2658+
value = kMonochromeTriggerId - 1;
2659+
}
2660+
const int frameCount = m_frameStore ? m_frameStore->count() : 0;
2661+
if (frameCount <= 0) {
2662+
return;
2663+
}
2664+
if (m_frameTriggerIds.size() < static_cast<std::size_t>(frameCount)) {
2665+
m_frameTriggerIds.resize(static_cast<std::size_t>(frameCount), 0xffffffffu);
2666+
}
2667+
const uint32_t triggerValue = (value < 0) ? 0xffffffffu : static_cast<uint32_t>(value);
2668+
const std::vector<int> targets = targetFrameIndices();
2669+
if (targets.empty()) {
2670+
return;
2671+
}
2672+
for (int row : targets) {
2673+
if (row < 0 || row >= static_cast<int>(m_frameTriggerIds.size())) {
2674+
continue;
2675+
}
2676+
m_frameTriggerIds[static_cast<std::size_t>(row)] = triggerValue;
2677+
}
2678+
if (m_hasLegacyRoundTrip) {
2679+
m_legacyRoundTrip.trigger_ids = m_frameTriggerIds;
2680+
}
2681+
});
25932682
connect(m_spriteDetAreaCombo, &QComboBox::currentIndexChanged, this, [this](int index) {
25942683
if (index < 0 || index >= MAX_SPRITE_DETECT_AREAS) {
25952684
return;
@@ -4751,6 +4840,7 @@ void MainWindow::openProjectFile(const QString& filename)
47514840
m_spriteStore->clear();
47524841
m_backgroundStore->clear();
47534842
m_frameDurations.clear();
4843+
m_frameTriggerIds.clear();
47544844
m_spriteNames.clear();
47554845
m_spriteColored.clear();
47564846
m_spriteColoredX.clear();
@@ -4814,6 +4904,15 @@ void MainWindow::openProjectFile(const QString& filename)
48144904
}
48154905
}
48164906
m_frameDurations = legacy.frame_durations;
4907+
m_frameTriggerIds = legacy.trigger_ids;
4908+
if (m_frameTriggerIds.size() < legacy.frames.size()) {
4909+
m_frameTriggerIds.resize(legacy.frames.size(), 0xffffffffu);
4910+
} else if (m_frameTriggerIds.size() > legacy.frames.size()) {
4911+
m_frameTriggerIds.resize(legacy.frames.size());
4912+
}
4913+
if (m_hasLegacyRoundTrip) {
4914+
m_legacyRoundTrip.trigger_ids = m_frameTriggerIds;
4915+
}
48174916
m_spriteNames = legacy.sprite_labels;
48184917
if (!m_serumDataLoaded) {
48194918
m_spriteColored = legacy.sprite_colored;
@@ -5118,11 +5217,14 @@ LegacyProject MainWindow::buildLegacyProject(const QString& baseName) const
51185217
}
51195218

51205219
const int frameCount = m_frameStore->count();
5220+
if (!m_frameTriggerIds.empty()) {
5221+
project.trigger_ids = m_frameTriggerIds;
5222+
}
51215223
if (m_hasLegacyRoundTrip) {
51225224
project.hash_codes.resize(static_cast<std::size_t>(frameCount), 0);
51235225
project.active_frames.resize(static_cast<std::size_t>(frameCount), 0);
5124-
project.trigger_ids.resize(static_cast<std::size_t>(frameCount), 0xffffffffu);
51255226
}
5227+
project.trigger_ids.resize(static_cast<std::size_t>(frameCount), 0xffffffffu);
51265228
project.frames.resize(static_cast<std::size_t>(frameCount));
51275229
cv::Size baseSize(kDefaultFrameWidth, kDefaultFrameHeight);
51285230
for (int i = 0; i < frameCount; ++i) {
@@ -5723,6 +5825,60 @@ void MainWindow::jumpPlayback(int index)
57235825
}
57245826
}
57255827

5828+
void MainWindow::updatePlaybackIdleFrame(int index)
5829+
{
5830+
if (!m_playbackCanvas || m_playbackActive) {
5831+
return;
5832+
}
5833+
if (index < 0) {
5834+
m_playbackCanvas->setTitle("Playback");
5835+
m_playbackCanvas->setImage(cv::Mat());
5836+
m_playbackCanvas->canvas()->setGridSegments(0, 0, 0);
5837+
m_playbackCanvas->canvas()->setGridScales(1, 1);
5838+
m_playbackCanvas->canvas()->setGridRegions(QRect(), QRect());
5839+
return;
5840+
}
5841+
const bool useHd = m_useHdFrame && hasHdFrame(index);
5842+
const cv::Mat composed = renderFrameWithSerum(index, useHd);
5843+
if (composed.empty()) {
5844+
m_playbackCanvas->setImage(cv::Mat());
5845+
m_playbackCanvas->canvas()->setGridSegments(0, 0, 0);
5846+
m_playbackCanvas->canvas()->setGridScales(1, 1);
5847+
m_playbackCanvas->canvas()->setGridRegions(QRect(), QRect());
5848+
return;
5849+
}
5850+
cv::Mat reference = buildOriginalPreviewForIndex(index);
5851+
cv::Mat original;
5852+
if (m_playbackShowOriginal && !reference.empty()) {
5853+
original = buildOriginalFrame(reference);
5854+
}
5855+
const QColor gap = m_playbackCanvas
5856+
? m_playbackCanvas->palette().color(QPalette::Window)
5857+
: QApplication::palette().color(QPalette::Window);
5858+
const cv::Scalar gapColor(gap.blue(), gap.green(), gap.red());
5859+
const cv::Mat combined = (!original.empty())
5860+
? buildCombinedFrame(composed, original, gapColor)
5861+
: EnsureBgr(composed);
5862+
m_playbackCanvas->setTitle(QString("Playback (frame %1)").arg(index + 1));
5863+
m_playbackCanvas->setImage(combined);
5864+
if (!original.empty()) {
5865+
const int gapPixels = FrameGapForWidth(composed.cols);
5866+
m_playbackCanvas->canvas()->setGridSegments(composed.rows, gapPixels, original.rows);
5867+
m_playbackCanvas->canvas()->setGridScales(1, 1);
5868+
FrameLayout layout = BuildFrameLayout(composed, original);
5869+
const QRect topRegion(layout.topX, 0, layout.topWidth, layout.topHeight);
5870+
const QRect bottomRegion(layout.bottomX,
5871+
layout.topHeight + gapPixels,
5872+
layout.bottomWidth,
5873+
layout.bottomHeight);
5874+
m_playbackCanvas->canvas()->setGridRegions(topRegion, bottomRegion);
5875+
} else {
5876+
m_playbackCanvas->canvas()->setGridSegments(0, 0, 0);
5877+
m_playbackCanvas->canvas()->setGridScales(1, 1);
5878+
m_playbackCanvas->canvas()->setGridRegions(QRect(), QRect());
5879+
}
5880+
}
5881+
57265882
void MainWindow::renderPlaybackFrame()
57275883
{
57285884
if (!m_playbackActive || m_playbackFrames.empty() || !m_playbackCanvas) {
@@ -5867,7 +6023,7 @@ void MainWindow::advancePlaybackFrame()
58676023

58686024
void MainWindow::updatePlaybackButtons()
58696025
{
5870-
const bool hasFrames = !m_frameDurations.empty();
6026+
const bool hasFrames = m_frameStore && m_frameStore->count() > 0;
58716027
if (m_previewPlayButton) {
58726028
m_previewPlayButton->setEnabled(hasFrames);
58736029
}
@@ -9474,6 +9630,12 @@ void MainWindow::ensureMaskDataSize()
94749630
m_frameDynamicCopyButton->setEnabled(hasFrames);
94759631
}
94769632
m_shapeCompToggle->setEnabled(hasFrames);
9633+
if (m_triggerIdSpin) {
9634+
m_triggerIdSpin->setEnabled(hasFrames);
9635+
}
9636+
if (m_triggerMonochromeCheck) {
9637+
m_triggerMonochromeCheck->setEnabled(hasFrames);
9638+
}
94779639
if (m_framesCanvas) {
94789640
m_framesCanvas->setMaskButtonsEnabled(hasFrames);
94799641
m_framesCanvas->setBackgroundMaskEnabled(hasFrames);
@@ -14539,8 +14701,8 @@ void MainWindow::refreshFrameSpriteLists()
1453914701
const QColor gap = m_spritesList->palette().color(QPalette::Window);
1454014702
const QStringList spriteNames = m_state->sprites();
1454114703
for (int i = 0; i < spriteNames.size(); ++i) {
14542-
const cv::Mat* image = m_spriteStore->at(i);
14543-
if (!image || image->empty()) {
14704+
const cv::Mat image = m_spriteStore->loadCopy(i);
14705+
if (image.empty()) {
1454414706
continue;
1454514707
}
1454614708
cv::Mat hd;
@@ -14549,7 +14711,7 @@ void MainWindow::refreshFrameSpriteLists()
1454914711
hd = *hdSprite;
1455014712
}
1455114713
}
14552-
cv::Mat previewMat = BuildBackgroundPreview(*image,
14714+
cv::Mat previewMat = BuildBackgroundPreview(image,
1455314715
hd,
1455414716
cv::Scalar(gap.blue(), gap.green(), gap.red()));
1455514717
if (previewMat.empty()) {
@@ -15151,6 +15313,7 @@ void MainWindow::showFrameAtIndex(int index)
1515115313
if (image && !image->empty()) {
1515215314
m_framesCanvas->canvas()->clearPreviewImage();
1515315315
updateFrameCanvasImage(index);
15316+
updatePlaybackIdleFrame(index);
1515415317
if (index == 0 && m_drawPointEnabled == false) {
1515515318
QTimer::singleShot(0, this, [this]() {
1515615319
m_framesCanvas->canvas()->requestFitOnResize(true);
@@ -15176,6 +15339,31 @@ void MainWindow::showFrameAtIndex(int index)
1517615339
const uint8_t value = m_frameShapeCompModes[static_cast<std::size_t>(index)];
1517715340
m_shapeCompToggle->setChecked(value != 0);
1517815341
}
15342+
if (m_triggerIdSpin && m_triggerMonochromeCheck) {
15343+
QSignalBlocker blockSpin(m_triggerIdSpin);
15344+
QSignalBlocker blockCheck(m_triggerMonochromeCheck);
15345+
if (index >= 0 && index < static_cast<int>(m_frameTriggerIds.size())) {
15346+
const uint32_t trigger = m_frameTriggerIds[static_cast<std::size_t>(index)];
15347+
const bool isMono = trigger == static_cast<uint32_t>(kMonochromeTriggerId);
15348+
m_triggerMonochromeCheck->setChecked(isMono);
15349+
m_triggerMonochromeCheck->setEnabled(true);
15350+
m_triggerIdSpin->setEnabled(true);
15351+
m_triggerIdSpin->setReadOnly(isMono);
15352+
if (isMono) {
15353+
m_triggerIdSpin->setValue(kMonochromeTriggerId);
15354+
} else if (trigger == 0xffffffffu) {
15355+
m_triggerIdSpin->setValue(-1);
15356+
} else {
15357+
m_triggerIdSpin->setValue(static_cast<int>(trigger));
15358+
}
15359+
} else {
15360+
m_triggerMonochromeCheck->setChecked(false);
15361+
m_triggerMonochromeCheck->setEnabled(false);
15362+
m_triggerIdSpin->setEnabled(false);
15363+
m_triggerIdSpin->setReadOnly(false);
15364+
m_triggerIdSpin->setValue(-1);
15365+
}
15366+
}
1517915367
const bool hasHd = hasHdFrame(index);
1518015368
if (!hasHd && m_useHdFrame) {
1518115369
m_useHdFrame = false;
@@ -15194,11 +15382,23 @@ void MainWindow::showFrameAtIndex(int index)
1519415382
refreshDynamicPaletteButtons();
1519515383
refreshRotationEditor();
1519615384
updateFrameUsageHighlights(index);
15385+
updatePlaybackButtons();
1519715386
} else {
1519815387
updateFrameCanvasImage(-1);
15388+
updatePlaybackIdleFrame(-1);
15389+
if (m_triggerIdSpin && m_triggerMonochromeCheck) {
15390+
QSignalBlocker blockSpin(m_triggerIdSpin);
15391+
QSignalBlocker blockCheck(m_triggerMonochromeCheck);
15392+
m_triggerMonochromeCheck->setChecked(false);
15393+
m_triggerMonochromeCheck->setEnabled(false);
15394+
m_triggerIdSpin->setEnabled(false);
15395+
m_triggerIdSpin->setReadOnly(false);
15396+
m_triggerIdSpin->setValue(-1);
15397+
}
1519915398
refreshFrameSpriteSlotCombo();
1520015399
refreshRotationEditor();
1520115400
updateFrameUsageHighlights(-1);
15401+
updatePlaybackButtons();
1520215402
}
1520315403
}
1520415404

app/MainWindow.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ class MainWindow : public QMainWindow
286286
void stopPlayback();
287287
void stepPlayback(int delta);
288288
void jumpPlayback(int index);
289+
void updatePlaybackIdleFrame(int index);
289290
void renderPlaybackFrame();
290291
void schedulePlaybackTick();
291292
void advancePlaybackFrame();
@@ -431,6 +432,8 @@ class MainWindow : public QMainWindow
431432
class QPushButton* m_frameDynamicCopyButton;
432433
class QComboBox* m_frameBackgroundAssign;
433434
class QCheckBox* m_shapeCompToggle;
435+
class QSpinBox* m_triggerIdSpin;
436+
class QCheckBox* m_triggerMonochromeCheck;
434437
class QComboBox* m_spriteDynamicSetCombo;
435438
class QComboBox* m_spriteDetAreaCombo;
436439
class QPushButton* m_spriteDetAreaClearButton;
@@ -504,6 +507,7 @@ class MainWindow : public QMainWindow
504507
std::vector<uint32_t> m_spriteColFromFrame;
505508
std::vector<uint16_t> m_spriteRects;
506509
std::vector<uint32_t> m_spriteRectMirror;
510+
std::vector<uint32_t> m_frameTriggerIds;
507511
std::vector<SpriteZoneGroup> m_spriteZones;
508512
std::vector<uint8_t> m_frameSpriteZoneFlags;
509513
int m_spriteZonePreferredSlot = -1;

0 commit comments

Comments
 (0)