Skip to content

Commit 0d7a54d

Browse files
committed
fixed optical photon screenshot rendering
1 parent 53b2e9b commit 0d7a54d

8 files changed

Lines changed: 115 additions & 65 deletions

File tree

gemc/actions/run/gRunAction.cc

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33
#include "gRun.h"
44
#include "../gactionConventions.h"
55
#include "gutsConventions.h"
6-
#include "g4display_options.h"
76

87
// geant4
98
#include "G4Threading.hh"
10-
#include "G4MTRunManager.hh"
11-
#include "G4UImanager.hh"
129

1310
std::mutex GRunAction::completed_run_data_mutex;
1411
GRunAction::CompletedRunData GRunAction::completed_worker_run_data;
@@ -22,11 +19,6 @@ GRunAction::GRunAction(std::shared_ptr<GOptions> gopt,
2219
digitization_routines_map(std::move(digi_map)) {
2320
const auto desc = std::to_string(G4Threading::G4GetThreadId());
2421
log->debug(CONSTRUCTOR, FUNCTION_NAME, desc);
25-
26-
auto g4view = g4display::getG4View(goptions);
27-
if (g4view.driver == "TOOLSSG_OFFSCREEN") {
28-
take_screenshots = true;
29-
}
3022
}
3123

3224

@@ -228,22 +220,6 @@ void GRunAction::EndOfRunAction(const G4Run *aRun) {
228220
}
229221
}
230222

231-
if (IsMaster() && take_screenshots) {
232-
take_screenshot(runNumber);
233-
}
234-
}
235-
236-
void GRunAction::take_screenshot(int runno) {
237-
auto ui = G4UImanager::GetUIpointer();
238-
ui->ApplyCommand("/vis/tsg/offscreen/set/size 3000 2000");
239-
240-
ui->ApplyCommand(
241-
"/vis/tsg/offscreen/set/file gemc_run_" +
242-
std::to_string(runno) + ".png"
243-
);
244-
245-
// This is what actually writes the PNG for TOOLSSG_OFFSCREEN.
246-
ui->ApplyCommand("/vis/viewer/rebuild");
247223
}
248224

249225
// Move this worker's completed run-data object into the protected static pool

gemc/actions/run/gRunAction.h

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,11 +330,6 @@ class GRunAction : public GBase<GRunAction>, public G4UserRunAction
330330

331331
// coming from digitization routine
332332
std::unordered_map<std::string, std::vector<std::string>> to_normalize;
333-
334-
335-
// screenshots
336-
bool take_screenshots = false;
337-
void take_screenshot(int runno);
338333
};
339334

340335

gemc/eventDispenser/eventDispenser.cc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ EventDispenser::EventDispenser(const std::shared_ptr<GOptions>&
3030
userRunno = gopt->getScalarInt("run");
3131
neventsToProcess = gopt->getScalarInt("n");
3232

33+
// Detect offscreen mode once at construction so processEvents() needs no vis headers.
34+
auto driverNode = gopt->getOptionMapInNode("g4view", "driver");
35+
if (!driverNode.IsNull() && driverNode.IsDefined()) {
36+
offscreen_screenshots = (driverNode.as<std::string>() == "TOOLSSG_OFFSCREEN");
37+
}
38+
3339
// If there are no events to process, keep the object in an initialized-but-idle state.
3440
if (neventsToProcess == 0) return;
3541

@@ -162,6 +168,16 @@ int EventDispenser::processEvents() {
162168
// The command string is a standard Geant4 UI command: \c /run/beamOn <N>.
163169
log->info(1, "Processing ", nevents, " events in one go");
164170
g4uim->ApplyCommand("/run/beamOn " + to_string(nevents));
171+
// Take the screenshot after BeamOn returns. At this point G4VisManager::EndOfRun()
172+
// has already joined the vis subthread (ARM64 offset 0xa35f8: bl thread::join), so
173+
// DrawEvent calls are finished — no concurrent scene-graph writes. The transient store
174+
// is still intact: the vis subthread's exit cleanup only runs after running=0 is set
175+
// inside G4VisManager::EndOfRun(), which completes inside BeamOn before it returns.
176+
if (offscreen_screenshots) {
177+
g4uim->ApplyCommand("/vis/tsg/offscreen/set/size 3000 2000");
178+
g4uim->ApplyCommand("/vis/tsg/offscreen/set/file gemc_run_" + to_string(runNumber) + ".png");
179+
g4uim->ApplyCommand("/vis/viewer/rebuild");
180+
}
165181

166182
log->info(1, "Run ", runNumber, " done with ", nevents, " events");
167183
}

gemc/eventDispenser/eventDispenser.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <gemc/gdynamicDigitization/gdynamicdigitization.h>
55

66
#include <map>
7+
#include <memory>
78
#include <vector>
89
#include <string>
910

@@ -60,6 +61,9 @@ class EventDispenser : public GBase<EventDispenser>
6061
/// Run number requested by the user (option \c -run) when not using a run-weight file.
6162
int userRunno;
6263

64+
/// True when the configured viewer is TOOLSSG_OFFSCREEN; triggers screenshot commands after BeamOn.
65+
bool offscreen_screenshots = false;
66+
6367
/// Most recently processed run number. Used to detect run changes and reload run-dependent data.
6468
int currentRunno = -1;
6569

gemc/g4display/g4SceneProperties.cc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@ std::vector<std::string> G4SceneProperties::scene_commands(const std::shared_ptr
5454
cmds.emplace_back("/vis/viewer/set/style surface");
5555
}
5656
if (gui || g4view.driver == "TOOLSSG_OFFSCREEN") {
57-
// Open the configured viewer driver with window geometry settings.
58-
cmds.emplace_back("/vis/open " + g4view.driver + " " + g4view.dimension + g4view.position);
57+
// Open the configured viewer driver. Offscreen drivers ignore window position, so omit it.
58+
std::string openArg = g4view.driver + " " + g4view.dimension;
59+
if (g4view.driver != "TOOLSSG_OFFSCREEN") openArg += g4view.position;
60+
cmds.emplace_back("/vis/open " + openArg);
5961

6062
// Convert configured camera angles to degrees for the Geant4 viewer command.
6163
const double toDegrees = 180.0 / M_PI;

gemc/gboard/gui_session.cc

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,55 +2,68 @@
22

33
// geant4
44
#include "G4UImanager.hh"
5+
#include "G4coutDestination.hh"
56

67
GUI_Session::GUI_Session(const std::shared_ptr<GOptions>& gopt, GBoard* b) :
78
GBase(gopt, GBOARD_LOGGER),
89
board(b) {
9-
// Route Geant4 UI output to this session instance, so we can forward it to the GUI board.
10+
// Route Geant4 UI output to this session instance so we can forward it to the GUI board.
11+
// SetCoutDestination updates the per-thread stream buffer but does NOT update
12+
// masterG4coutDestination, which G4UIQt set to itself in its constructor.
13+
// Worker threads use G4MasterForwardcoutDestination -> masterG4coutDestination, so
14+
// we must update it here to prevent G4UIQt::ReceiveG4cerr from being called on a
15+
// background thread (which would trigger an NSAlert from a non-main thread on macOS).
1016
G4UImanager::GetUIpointer()->SetCoutDestination(this);
17+
G4coutDestination::masterG4coutDestination = this;
1118

1219
log->info(1, SFUNCTION_NAME, " g4 dialog : GUI_Session created");
1320
}
1421

1522
G4int GUI_Session::ReceiveG4cout(const G4String& coutString) {
1623
// See header for API docs.
17-
if (board) {
18-
QString fullQString = QString::fromStdString(coutString);
19-
20-
// Split into lines so that the board gets "log-like" incremental entries.
21-
// KeepEmptyParts preserves blank lines, while avoiding QRegularExpression
22-
// compatibility issues with Unicode line separator escapes.
23-
fullQString.replace("\r\n", "\n");
24-
fullQString.replace('\r', '\n');
25-
fullQString.replace(QChar(0x2028), '\n');
26-
QStringList lines = fullQString.split('\n', Qt::KeepEmptyParts);
27-
28-
for (const QString& line : lines) {
29-
// Convert ANSI attributes (if present) into HTML for rich-text display.
30-
QString htmlLine = ansiToHtml(line);
31-
board->appendLog(htmlLine);
32-
}
33-
}
24+
if (!board) return 0;
25+
26+
QString fullQString = QString::fromStdString(coutString);
27+
// Split into lines so that the board gets "log-like" incremental entries.
28+
fullQString.replace("\r\n", "\n");
29+
fullQString.replace('\r', '\n');
30+
fullQString.replace(QChar(0x2028), '\n');
31+
QStringList lines = fullQString.split('\n', Qt::KeepEmptyParts);
32+
33+
// Convert ANSI to HTML on the calling thread (no Qt object access needed).
34+
QStringList htmlLines;
35+
htmlLines.reserve(lines.size());
36+
for (const QString& line : lines) { htmlLines << ansiToHtml(line); }
37+
38+
// Post widget update to the main thread. Qt::AutoConnection calls directly
39+
// when already on the main thread, and queues the call otherwise, preventing
40+
// NSWindow/NSAlert operations from a Geant4 worker thread on macOS.
41+
auto* b = board;
42+
QMetaObject::invokeMethod(b, [b, htmlLines]() {
43+
for (const QString& htmlLine : htmlLines) { b->appendLog(htmlLine); }
44+
});
3445
return 0;
3546
}
3647

3748

3849
G4int GUI_Session::ReceiveG4cerr(const G4String& cerrString) {
3950
// See header for API docs.
40-
if (board) {
41-
QString fullQString = QString::fromStdString(cerrString);
42-
43-
// Use the same line normalization as stdout.
44-
fullQString.replace("\r\n", "\n");
45-
fullQString.replace('\r', '\n');
46-
fullQString.replace(QChar(0x2028), '\n');
47-
QStringList lines = fullQString.split('\n', Qt::KeepEmptyParts);
48-
49-
for (const QString& line : lines) {
50-
QString htmlLine = ansiToHtml(line);
51-
board->appendLog(htmlLine);
52-
}
53-
}
51+
if (!board) return 0;
52+
53+
QString fullQString = QString::fromStdString(cerrString);
54+
fullQString.replace("\r\n", "\n");
55+
fullQString.replace('\r', '\n');
56+
fullQString.replace(QChar(0x2028), '\n');
57+
QStringList lines = fullQString.split('\n', Qt::KeepEmptyParts);
58+
59+
QStringList htmlLines;
60+
htmlLines.reserve(lines.size());
61+
for (const QString& line : lines) { htmlLines << ansiToHtml(line); }
62+
63+
auto* b = board;
64+
QMetaObject::invokeMethod(b, [b, htmlLines]() {
65+
for (const QString& htmlLine : htmlLines) { b->appendLog(htmlLine); }
66+
});
5467
return 0;
5568
}
5669

@@ -211,8 +224,12 @@ GUI_Session::~GUI_Session() {
211224
// See header for API docs.
212225
// Detach Geant4 cout/cerr from our GUI session (avoid dangling callback).
213226
if (auto* UIM = G4UImanager::GetUIpointer()) {
214-
// If available in your G4 version, prefer checking the current destination:
215-
// if (UIM->GetCoutDestination() == gui_session.get()) { ... }
216227
UIM->SetCoutDestination(nullptr);
217228
}
229+
// Also clear masterG4coutDestination, which we claimed in the constructor.
230+
// Worker threads read this pointer at call time, so nulling it here prevents
231+
// any in-flight forwarding from reaching the already-destroyed session.
232+
if (G4coutDestination::masterG4coutDestination == this) {
233+
G4coutDestination::masterG4coutDestination = nullptr;
234+
}
218235
}

gemc/utilities/gemcUtilities.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ namespace gemc {
140140
cmds.emplace_back("/vis/modeling/trajectories/create/drawByCharge");
141141
cmds.emplace_back("/vis/modeling/trajectories/drawByCharge-0/default/setDrawStepPts true");
142142
cmds.emplace_back("/vis/modeling/trajectories/drawByCharge-0/default/setStepPtsSize 2");
143+
// Draw optical photons in cyan so they are distinct from other neutral particles.
144+
cmds.emplace_back("/vis/modeling/trajectories/create/drawByParticleID");
145+
cmds.emplace_back("/vis/modeling/trajectories/drawByParticleID-0/set opticalphoton cyan");
146+
143147
cmds.emplace_back("/vis/scene/add/hits");
144148
cmds.emplace_back("/vis/scene/endOfEventAction accumulate 10000");
145149
cmds.push_back("/vis/viewer/set/background " + g4view.background);

releases/0.3.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ This version includes:
1212
- `GEMC_PLUGIN_PATH` and `-plugin_path` for external plugin discovery
1313
- Pre-parse plugin options hook for dynamic plugin option registration
1414
- ROOT gstreamer and event-action bug fixes
15+
- Fixed TOOLSSG_OFFSCREEN batch crash (race condition between screenshot rendering and vis subthread)
16+
- Optical photon trajectories rendered in cyan in both GUI and offscreen screenshots
1517
- Fixed geometry-tree opacity slider resetting volume color to original
1618
- Plotting tests for digitizing-SD examples and Cherenkov photon hit maps
1719

@@ -57,6 +59,19 @@ This version includes:
5759
`BeamOn`.
5860
- Isolated ROOT linkage to the ROOT gstreamer plugin so the main `gemc`
5961
executable can run without ROOT shared libraries.
62+
- Fixed intermittent `SIGSEGV` (pointer-authentication failure / bus error) in batch mode with
63+
`TOOLSSG_OFFSCREEN`: `GRunAction::EndOfRunAction` called `take_screenshot()``/vis/viewer/rebuild`
64+
`G4ToolsSGOffscreenViewer::DrawView()` while the Geant4 vis subthread
65+
(`G4VisManager::G4VisSubThread`) was concurrently running `ClearTransientStore()` and freeing the
66+
same scene-graph nodes. Geant4 11.4 joins the vis subthread inside `G4VisManager::EndOfRun()`,
67+
which is called *after* `EndOfRunAction`, so the race window was real. The fix moves the three
68+
screenshot UImanager commands out of `GRunAction::EndOfRunAction` and into
69+
`EventDispenser::processEvents()`, issued immediately after each `/run/beamOn` returns — at that
70+
point `G4VisManager::EndOfRun()` has already run and the vis subthread is guaranteed to have been
71+
joined.
72+
- Added a `drawByParticleID opticalphoton cyan` trajectory model to the visualization setup so optical
73+
photon tracks appear in cyan in both interactive and offscreen screenshots, distinguishing them from
74+
other neutral particles that would otherwise share the default neutral-particle color.
6075
- Fixed GUI crash (SIGABRT in `G4ToolsSGSceneHandler::GetOrCreateNode`) when
6176
reloading geometry after running events: `refreshGeometryTree` now tracks
6277
whether a viewer already exists and skips `/vis/open` on subsequent reloads,
@@ -443,3 +458,24 @@ Both x86_64 and ARM64 platforms are supported.
443458
`const QColor&`. The twinkle restore branch in `onTwinkleStep` now also calls
444459
`item->set_color(twinkleSavedColor)` and `item->set_opacity(twinkleSavedOpacity)` before
445460
issuing the restore command, keeping the model consistent after animation.
461+
- Fixed intermittent `SIGSEGV` (bus error / pointer-authentication failure on ARM64) in batch mode
462+
when using `TOOLSSG_OFFSCREEN`. The crash was a data race between the main thread in
463+
`GRunAction::EndOfRunAction` → `take_screenshot()` → `G4ToolsSGOffscreenViewer::DrawView()` →
464+
`tools::sg::separator::render()` traversing scene-graph nodes, and `G4VisManager::G4VisSubThread`
465+
concurrently running `G4ToolsSGSceneHandler::ClearTransientStore()` → `_xzm_free()` on the same
466+
nodes. Disassembly of `G4VisManager::EndOfRun()` confirmed that the vis subthread `thread::join()`
467+
happens inside `EndOfRun()`, which Geant4 11.4 calls *after* `GRunAction::EndOfRunAction()`, leaving
468+
a window where the render and the free overlap. The fix removes `take_screenshot()` from
469+
`GRunAction` entirely and moves the three screenshot UImanager commands into
470+
`EventDispenser::processEvents()`, after each `g4uim->ApplyCommand("/run/beamOn ...")` returns.
471+
By that point `G4VisManager::EndOfRun()` has already been called and the vis subthread is joined,
472+
so the scene graph is stable. The `TOOLSSG_OFFSCREEN` check is resolved once at construction from
473+
the `g4view.driver` option node so `EventDispenser` acquires no dependency on the `g4display`
474+
library.
475+
- Added a `drawByParticleID` trajectory model alongside the existing `drawByCharge` model in
476+
`gemcUtilities::initial_commands()`. Optical photon trajectories are assigned the color cyan via
477+
`/vis/modeling/trajectories/create/drawByParticleID` and
478+
`/vis/modeling/trajectories/drawByParticleID-0/set opticalphoton cyan`. Without this model, optical
479+
photons inherit the `drawByCharge` neutral-particle default color, making them indistinguishable
480+
from neutrons and other neutral tracks — particularly against the light-colored default background
481+
used in screenshots and the GUI.

0 commit comments

Comments
 (0)