wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
ICRegistry.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <QCoreApplication>
7#include <QDataStream>
8#include <QDir>
9#include <QFileInfo>
10#include <QSaveFile>
11#include <QThread>
12
13#include "App/Core/Common.h"
15#include "App/Element/IC.h"
18#include "App/IO/VersionInfo.h"
19#include "App/Scene/Commands.h"
20#include "App/Scene/Scene.h"
23#include "App/Versions.h"
24
26 : QObject(scene)
27 , m_scene(scene)
28{
29 connect(&m_fileWatcher, &QFileSystemWatcher::fileChanged,
30 this, &ICRegistry::onFileChanged, Qt::QueuedConnection);
31}
32
33const QByteArray &ICRegistry::cachedFileBytes(const QString &filePath)
34{
35 Q_ASSERT(QCoreApplication::instance()->thread() == QThread::currentThread());
36
37 if (!m_fileCache.contains(filePath)) {
38 QFile file(filePath);
39 if (file.open(QIODevice::ReadOnly)) {
40 m_fileCache[filePath] = file.readAll();
41 } else {
42 // Do NOT insert into m_fileCache on failure: QMap::operator[] would
43 // default-construct and permanently cache an empty entry, silently
44 // masking the failure on every subsequent lookup (including if the
45 // file becomes readable later).
46 qCWarning(zero) << "ICRegistry: cannot open IC file:" << filePath;
47 static const QByteArray empty;
48 return empty;
49 }
50 }
51 return m_fileCache[filePath];
52}
53
54void ICRegistry::invalidate(const QString &filePath)
55{
56 m_fileCache.remove(filePath);
57}
58
59void ICRegistry::watchFile(const QString &filePath)
60{
61 if (!m_fileWatcher.files().contains(filePath)) {
62 m_fileWatcher.addPath(filePath);
63 }
64}
65
66QList<GraphicElement *> ICRegistry::findICsByFile(const QString &fileName) const
67{
68 const QFileInfo target(fileName);
69 QList<GraphicElement *> result;
70 for (auto *elm : m_scene->elements()) {
71 if (elm->elementType() == ElementType::IC) {
72 auto *ic = static_cast<IC *>(elm);
73 if (QFileInfo(ic->file()) == target) {
74 result.append(elm);
75 }
76 }
77 }
78 return result;
79}
80
81void ICRegistry::onFileChanged(const QString &filePath)
82{
83 qCDebug(zero) << "IC file changed:" << filePath;
84
85 // Invalidate the cached definition so it's rebuilt on next access
86 invalidate(filePath);
87
88 // Re-add the watch (some OS remove it after a file change event)
89 if (!m_fileWatcher.files().contains(filePath) && QFileInfo::exists(filePath)) {
90 m_fileWatcher.addPath(filePath);
91 }
92
93 // Reload all IC instances referencing this file
94 const auto targets = findICsByFile(filePath);
95 if (targets.isEmpty()) {
96 emit definitionChanged(filePath);
97 return;
98 }
99
100 // Capture pre-reload state so the undo command can restore both the
101 // ICs' element data and the scene wires that touch their ports.
102 // Without this the wires get cascade-deleted by setInputSize/setOutputSize
103 // inside loadFile and Cluster D throws on the next undo lookup.
104 const auto connections = UpdateBlobCommand::captureConnections(targets);
105 const QByteArray oldData = captureSnapshot(targets);
106
107 try {
108 reloadTargetsAtomically(targets, oldData, [&](IC *ic) { ic->loadFile(filePath); });
109 } catch (...) {
110 m_scene->setCircuitUpdateRequired();
111 emit definitionChanged(filePath);
112 throw;
113 }
114 m_scene->setCircuitUpdateRequired();
115
116 auto *cmd = new UpdateBlobCommand(targets, oldData, connections, m_scene);
117 m_scene->undoStack()->push(cmd);
118
119 emit definitionChanged(filePath);
120}
121
122// --- Embedded IC blob storage ---
123
124bool ICRegistry::hasBlob(const QString &name) const
125{
126 return m_blobs.contains(name);
127}
128
129QByteArray ICRegistry::blob(const QString &name) const
130{
131 return m_blobs.value(name);
132}
133
134void ICRegistry::setBlob(const QString &name, const QByteArray &data)
135{
136 m_blobs[name] = data;
137}
138
139void ICRegistry::registerBlob(const QString &name, const QByteArray &data)
140{
141 QSet<QString> visited;
142 QMap<QString, QByteArray> workingBlobs;
143 workingBlobs[name] = data;
144 makeBlobSelfContained(name, visited, workingBlobs);
145 m_blobs[name] = workingBlobs[name];
146}
147
148void ICRegistry::removeBlob(const QString &name)
149{
150 m_blobs.remove(name);
151}
152
153void ICRegistry::renameBlob(const QString &oldName, const QString &newName)
154{
155 // Defense in depth: the primary guard is at the caller (ElementEditor rejects a colliding
156 // rename before ever constructing a command), but no-op here too rather than silently
157 // overwriting an unrelated blob's bytes, so any future/other caller can't corrupt one IC's
158 // data by renaming a different one onto it.
159 if (!m_blobs.contains(oldName) || oldName == newName || m_blobs.contains(newName)) {
160 return;
161 }
162
163 m_blobs[newName] = m_blobs.take(oldName);
164
165 // Update all IC instances on the scene referencing the old name
166 for (auto *elm : m_scene->elements()) {
167 if (elm->isEmbedded() && elm->blobName() == oldName) {
168 auto *ic = static_cast<IC *>(elm);
169 ic->setBlobName(newName);
170 }
171 }
172
173 // Update embedded IC references inside other blobs' metadata so that parent
174 // blobs that contain the renamed blob as a nested dependency stay consistent.
175 for (auto it = m_blobs.begin(); it != m_blobs.end(); ++it) {
176 renameBlobReference(it.value(), oldName, newName);
177 }
178
179 emit blobRenamed(oldName, newName);
180}
181
183{
184 m_blobs.clear();
185}
186
187QList<GraphicElement *> ICRegistry::findICsByBlobName(const QString &blobName) const
188{
189 QList<GraphicElement *> result;
190 for (auto *elm : m_scene->elements()) {
191 if (elm->isEmbedded() && elm->blobName() == blobName) {
192 result.append(elm);
193 }
194 }
195 return result;
196}
197
198bool ICRegistry::initEmbeddedIC(IC *ic, const QString &blobName)
199{
200 if (!hasBlob(blobName)) {
201 return false;
202 }
203 ic->setBlobName(blobName);
204 ic->loadFromBlob(blob(blobName), m_scene->contextDir());
205 if (ic->label().isEmpty()) {
206 ic->setLabel(blobName.toUpper());
207 }
208 return true;
209}
210
211QString ICRegistry::uniqueBlobName(const QString &baseName) const
212{
213 if (!hasBlob(baseName)) {
214 return baseName;
215 }
216 for (int i = 2; ; ++i) {
217 const QString candidate = baseName + "_" + QString::number(i);
218 if (!hasBlob(candidate)) {
219 return candidate;
220 }
221 }
222}
223
224IC *ICRegistry::createEmbeddedIC(const QString &blobName, const QByteArray &fileBytes, const QString &contextDir)
225{
226 auto *ic = new IC();
227 ic->setBlobName(blobName);
228
229 m_scene->undoStack()->beginMacro(QCoreApplication::tr("Add embedded IC"));
230 m_scene->receiveCommand(new RegisterBlobCommand(blobName, fileBytes, m_scene));
231 ic->loadFromBlob(fileBytes, contextDir);
232 m_scene->receiveCommand(new AddItemsCommand({ic}, m_scene));
233 m_scene->undoStack()->endMacro();
234
235 return ic;
236}
237
238int ICRegistry::embedICsByFile(const QString &fileName, const QByteArray &fileBytes,
239 const QString &blobName)
240{
241 const auto targets = findICsByFile(fileName);
242 if (targets.isEmpty()) {
243 return 0;
244 }
245
246 const auto connections = UpdateBlobCommand::captureConnections(targets);
247 const QByteArray oldData = captureSnapshot(targets);
248
249 registerBlob(blobName, fileBytes);
250
251 try {
252 reloadTargetsAtomically(targets, oldData, [&](IC *ic) {
253 ic->setBlobName(blobName);
254 ic->loadFromBlob(m_blobs[blobName], m_scene->contextDir());
255 });
256 } catch (...) {
257 removeBlob(blobName);
258 throw;
259 }
260
261 auto *cmd = new UpdateBlobCommand(targets, oldData, connections, m_scene);
262 // This blob is newly registered above (registerBlob() at a name these targets weren't
263 // already using), not replacing prior content — leave m_oldBlob at its default-empty so
264 // undo() removes it rather than restoring bytes that never existed. Explicit rather than
265 // relying on the constructor default, unlike every other UpdateBlobCommand call site.
266 cmd->setOldBlob(QByteArray());
267 m_scene->undoStack()->push(cmd);
268 return static_cast<int>(targets.size());
269}
270
271int ICRegistry::extractToFile(const QString &blobName, const QString &filePath)
272{
273 // Write blob to disk
274 QSaveFile saveFile(filePath);
275 if (!saveFile.open(QIODevice::WriteOnly)) {
276 throw PANDACEPTION("Could not open file: %1", saveFile.errorString());
277 }
278 saveFile.write(blob(blobName));
279 if (!saveFile.commit()) {
280 throw PANDACEPTION("Could not save file: %1", saveFile.errorString());
281 }
282
283 // Convert all embedded ICs with this blobName to file-backed
284 const auto targets = findICsByBlobName(blobName);
285 if (targets.isEmpty()) {
286 return 0;
287 }
288
289 const auto connections = UpdateBlobCommand::captureConnections(targets);
290 const QByteArray oldData = captureSnapshot(targets);
291 const QByteArray oldBlob = blob(blobName);
292
293 const QString fileDir = QFileInfo(filePath).absolutePath();
294 reloadTargetsAtomically(targets, oldData, [&](IC *ic) { ic->loadFile(filePath, fileDir); });
295
296 removeBlob(blobName);
297
298 auto *cmd = new UpdateBlobCommand(targets, oldData, connections, m_scene);
299 cmd->setOldBlob(oldBlob);
300 cmd->setBlobName(blobName);
301 m_scene->undoStack()->push(cmd);
302 return static_cast<int>(targets.size());
303}
304
305void ICRegistry::rollbackElements(const QList<GraphicElement *> &elements, const QByteArray &snapshot,
306 Scene *scene)
307{
308 QByteArray data(snapshot);
309 QDataStream stream(&data, QIODevice::ReadOnly);
310 const auto version = Serialization::readPandaHeader(stream);
311 QHash<quint64, Port *> portMap;
312 auto ctx = scene->deserializationContext(portMap, version, SerializationPurpose::InMemorySnapshot);
313 for (auto *elm : elements) {
314 elm->load(stream, ctx);
315 }
316}
317
318void ICRegistry::reloadTargetsAtomically(const QList<GraphicElement *> &targets, const QByteArray &oldData,
319 const std::function<void(IC *)> &mutate)
320{
321 // Stop the simulation for the entire loop, not just each individual mutation. Between
322 // freeing one IC's old internal graph and rebuilding it, the scene's sorted vectors hold
323 // dangling pointers; ticking on that state would fault — and that stale state persists
324 // across the whole loop, since setCircuitUpdateRequired() only runs once, after every
325 // target has been reloaded, not after each one.
326 QList<GraphicElement *> updated;
327 SimulationBlocker blocker(m_scene->simulation());
328 try {
329 for (auto *elm : targets) {
330 mutate(static_cast<IC *>(elm));
331 updated.append(elm);
332 }
333 } catch (...) {
334 rollbackElements(updated, oldData, m_scene);
335 throw;
336 }
337}
338
339void ICRegistry::makeBlobSelfContained(const QString &name, QSet<QString> &visited,
340 QMap<QString, QByteArray> &blobs, int depth)
341{
342 if (depth >= kMaxBlobNestingDepth) {
343 throw PANDACEPTION("Embedded IC dependency chain exceeds the maximum nesting depth (%1) while resolving '%2'",
344 QString::number(kMaxBlobNestingDepth), name);
345 }
346
347 if (visited.contains(name)) {
348 qCWarning(zero) << "Circular blob reference detected:" << name << "— skipping.";
349 return;
350 }
351 visited.insert(name);
352
353 QByteArray blobData(blobs[name]);
354 QDataStream readStream(&blobData, QIODevice::ReadOnly);
355 const auto preamble = Serialization::readPreamble(readStream);
356
357 if (!VersionInfo::hasMetadata(preamble.version)) {
358 return;
359 }
360
361 auto metadata = preamble.metadata;
362 auto embeddedICs = Serialization::deserializeBlobRegistry(metadata, preamble.version);
363 bool modified = false;
364
365 // Recurse into already-embedded blobs to ensure they are self-contained
366 for (auto it = embeddedICs.begin(); it != embeddedICs.end(); ++it) {
367 const QString &depName = it.key();
368 blobs[depName] = it.value();
369 makeBlobSelfContained(depName, visited, blobs, depth + 1);
370 it.value() = blobs[depName];
371 }
372
373 // Resolve file-backed IC dependencies from disk and embed them
374 if (metadata.contains("fileBackedICs")) {
375 const QStringList files = metadata.value("fileBackedICs").toStringList();
376 const QString contextDir = m_scene->contextDir();
377
378 for (const QString &fileName : files) {
379 const QString baseName = QFileInfo(fileName).baseName();
380 if (embeddedICs.contains(baseName)) {
381 continue;
382 }
383
384 QFileInfo fi(QDir(contextDir), fileName);
385 if (!fi.exists()) {
386 qCWarning(zero) << "makeBlobSelfContained: dependency" << fileName
387 << "not found for blob" << name << "— skipping.";
388 continue;
389 }
390
391 QFile file(fi.absoluteFilePath());
392 if (!file.open(QIODevice::ReadOnly)) {
393 qCWarning(zero) << "makeBlobSelfContained: cannot open dependency" << fi.absoluteFilePath()
394 << "for blob" << name << "— blob will be incomplete.";
395 continue;
396 }
397 QByteArray fileBytes = file.readAll();
398 file.close();
399
400 // Recursively make the dep self-contained before embedding
401 blobs[baseName] = fileBytes;
402 makeBlobSelfContained(baseName, visited, blobs, depth + 1);
403 embeddedICs[baseName] = blobs[baseName];
404 }
405
406 metadata.remove("fileBackedICs");
407 modified = true;
408 }
409
410 if (!modified) {
411 return;
412 }
413
414 // Re-serialize the blob with updated metadata, preserving the elements/connections
415 // tail byte-for-byte. Read that tail from the already-decompressed
416 // preamble.remainingPayload, not readStream's device -- readPreamble() fully
417 // consumed the live device to do that decompression, so the device has
418 // nothing left to offer here.
419 const QByteArray elements = preamble.remainingPayload;
420
421 Serialization::serializeBlobRegistry(embeddedICs, metadata);
422
423 // Metadata and the elements tail must be compressed together as one payload
424 // (see Serialization::writePayload()), not written as two independent raw
425 // writes -- readPreamble() on the other end decompresses the whole thing in
426 // one shot.
427 QByteArray payload;
428 QDataStream payloadStream(&payload, QIODevice::WriteOnly);
429 payloadStream.setVersion(QDataStream::Qt_5_12);
430 payloadStream << metadata;
431 payloadStream.writeRawData(elements.constData(), static_cast<int>(elements.size()));
432
433 QByteArray newBlob;
434 QDataStream writeStream(&newBlob, QIODevice::WriteOnly);
436 Serialization::writePayload(writeStream, payload);
437
438 blobs[name] = newBlob;
439}
440
441void ICRegistry::renameBlobReference(QByteArray &blobData, const QString &oldName, const QString &newName)
442{
443 QDataStream readStream(&blobData, QIODevice::ReadOnly);
444 const auto preamble = Serialization::readPreamble(readStream);
445
446 if (!VersionInfo::hasMetadata(preamble.version)) {
447 return;
448 }
449
450 auto embeddedICs = Serialization::deserializeBlobRegistry(preamble.metadata, preamble.version);
451 if (!embeddedICs.contains(oldName)) {
452 return;
453 }
454
455 // Rename the key in the embedded IC map
456 embeddedICs[newName] = embeddedICs.take(oldName);
457
458 // Re-serialize the blob with updated metadata, preserving the elements/connections
459 // tail byte-for-byte. Read that tail from the already-decompressed
460 // preamble.remainingPayload, not readStream's device -- readPreamble() fully
461 // consumed the live device to do that decompression, so the device has
462 // nothing left to offer here.
463 const QByteArray elements = preamble.remainingPayload;
464 auto metadata = preamble.metadata;
465 Serialization::serializeBlobRegistry(embeddedICs, metadata);
466
467 // Metadata and the elements tail must be compressed together as one payload
468 // (see Serialization::writePayload()), not written as two independent raw
469 // writes -- readPreamble() on the other end decompresses the whole thing in
470 // one shot.
471 QByteArray payload;
472 QDataStream payloadStream(&payload, QIODevice::WriteOnly);
473 payloadStream.setVersion(QDataStream::Qt_5_12);
474 payloadStream << metadata;
475 payloadStream.writeRawData(elements.constData(), static_cast<int>(elements.size()));
476
477 QByteArray newBlob;
478 QDataStream writeStream(&newBlob, QIODevice::WriteOnly);
480 Serialization::writePayload(writeStream, payload);
481
482 blobData = newBlob;
483}
484
485QByteArray ICRegistry::captureSnapshot(const QList<GraphicElement *> &targets)
486{
487 QByteArray data;
488 QDataStream stream(&data, QIODevice::WriteOnly);
490 for (auto *elm : targets) {
491 elm->save(stream, {.purpose = SerializationPurpose::InMemorySnapshot});
492 }
493 return data;
494}
All QUndoCommand subclasses and the CommandUtils helper namespace.
Common logging utilities, the Pandaception error type, and helper macros.
#define PANDACEPTION(msg,...)
Definition Common.h:98
#define qCDebug(category)
Definition Common.h:29
Abstract base class for all graphical circuit elements.
IC definition registry with file watching and embedded blob storage.
Integrated Circuit (IC) graphic element that encapsulates a sub-circuit file.
Main circuit editing scene with undo/redo and user interaction.
Deserialization/serialization context structs passed through load()/save() call chains.
Circuit and waveform file serialization/deserialization utilities.
RAII guard that temporarily stops the simulation while in scope.
Synchronous cycle-based simulation engine with event-driven clock support.
Named version predicates for file-format compatibility checks.
File-format version constants and application version accessor.
Undo command that adds a list of graphic elements to the scene.
Definition Commands.h:67
QString label() const
Returns the user-visible label text for this element.
void setLabel(const QString &label)
Sets the label text to label and refreshes the display.
void renameBlob(const QString &oldName, const QString &newName)
Renames a blob from oldName to newName, updating the cache key.
void setBlob(const QString &name, const QByteArray &data)
Stores or replaces the blob data under name and invalidates the cached definition.
ICRegistry(Scene *scene)
int embedICsByFile(const QString &fileName, const QByteArray &fileBytes, const QString &blobName)
Converts all file-backed IC elements referencing fileName to embedded ICs using blobName.
bool initEmbeddedIC(IC *ic, const QString &blobName)
Initializes an embedded IC by looking up its blob in the registry.
QByteArray blob(const QString &name) const
Returns the raw .panda bytes for the embedded IC named name.
void clearBlobs()
Removes all stored blobs.
QString uniqueBlobName(const QString &baseName) const
Returns baseName if available, or appends a numeric suffix to avoid collision.
QList< GraphicElement * > findICsByBlobName(const QString &blobName) const
Finds all embedded IC elements with blobName.
int extractToFile(const QString &blobName, const QString &filePath)
Writes the blob to disk and converts all embedded ICs with blobName to file-backed.
void invalidate(const QString &filePath)
Invalidates a cached definition (e.g., after file change).
const QByteArray & cachedFileBytes(const QString &filePath)
Returns cached file bytes, reading from disk on first access. Returns empty on failure.
void definitionChanged(const QString &filePath)
Emitted when an IC definition file changes on disk and its cached definition is invalidated.
void blobRenamed(const QString &oldName, const QString &newName)
void removeBlob(const QString &name)
Removes the blob named name from the registry.
IC * createEmbeddedIC(const QString &blobName, const QByteArray &fileBytes, const QString &contextDir)
Creates a new embedded IC from file bytes, registers the blob, and pushes an undo command.
QList< GraphicElement * > findICsByFile(const QString &fileName) const
Finds all IC elements in the scene that reference fileName.
static void rollbackElements(const QList< GraphicElement * > &elements, const QByteArray &snapshot, Scene *scene)
Restores elements from a previously captured snapshot (used for atomic rollback).
void registerBlob(const QString &name, const QByteArray &data)
Stores blob data under name without invalidating the definition cache.
void watchFile(const QString &filePath)
Registers a file for watching. Called when an IC element is added to the scene.
bool hasBlob(const QString &name) const
Returns true if a blob named name is stored in the registry.
static QByteArray captureSnapshot(const QList< GraphicElement * > &targets)
Serializes targets into a self-contained .panda byte array (used for embedding).
Graphic element representing an Integrated Circuit (sub-circuit) box.
Definition IC.h:31
void loadFromBlob(const QByteArray &blob, const QString &contextDir)
Loads the IC from in-memory blob bytes (full .panda file format).
Definition IC.cpp:248
void setBlobName(const QString &name)
Definition IC.h:80
void loadFile(const QString &fileName, const QString &contextDir={})
Loads the IC circuit from fileName and rebuilds the logic mapping.
Definition IC.cpp:243
Undo command that registers/unregisters a blob in the IC registry.
Definition Commands.h:450
Main circuit editing scene.
Definition Scene.h:56
SerializationContext deserializationContext(QHash< quint64, Port * > &portMap, const QVersionNumber &version, SerializationPurpose purpose)
Definition Scene.cpp:216
Simulation * simulation()
Returns the simulation engine associated with this scene.
Definition Scene.cpp:288
static void writePandaHeader(QDataStream &stream)
Writes the .panda circuit file header to stream.
static void serializeBlobRegistry(const QMap< QString, QByteArray > &blobs, QMap< QString, QVariant > &metadata)
Serializes embedded ICs into a metadata map (sets the "embeddedICs" key).
static QVersionNumber readPandaHeader(QDataStream &stream)
Reads and validates the .panda circuit file header; returns the stored version number.
static void writePayload(QDataStream &stream, const QByteArray &payload)
Compresses payload (qCompress) and writes it to stream.
static Preamble readPreamble(QDataStream &stream)
Reads the full .panda preamble: header, dolphin filename, rect, and metadata (V_4_5+).
static QMap< QString, QByteArray > deserializeBlobRegistry(const QMap< QString, QVariant > &metadata, const QVersionNumber &fileVersion)
Extracts the embedded IC registry from a metadata map.
RAII guard that stops the simulation on construction and restarts it on destruction.
Undo command for embedded IC blob changes that may alter port counts.
Definition Commands.h:521
static QList< ConnectionInfo > captureConnections(const QList< GraphicElement * > &targets)
Captures connection topology for all target elements before a blob operation.
bool hasMetadata(const QVersionNumber &v)
V4.5: File-level metadata map and embedded IC blob registry.
Definition VersionInfo.h:72