Revision control
Copy as Markdown
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Takes the scene away from the world from inside a trigger report, while the
// batch of reports that carries it is still on the stack. Moving the world
// deregisters every body at once, which is what deleting one does to the
// trigger callback's end of things, and it lands while the callback is still
// running rather than whenever the object a delete was asked for is deferred
// to. Deregistering from there took the lock the callback held, on the thread
// that already held it, so the application hung. Reaching the next frame is
// the assertion.
//
// Three bodies cross the boundary together, so the report the world is moved
// from is one of a batch and the pairs behind it are about bodies the move has
// taken out. Only the first is reported: the rest are asked about one at a
// time and skipped.
//
// No rendering is needed for any of this, so scene is a plain Node rather than
// a View3D.
import QtQuick
import QtTest
import QtQuick3D
import QtQuick3D.Physics
import QtQuick3D.Physics.TestUtils
Item {
id: root
width: 64
height: 64
// The frame the world was moved on, set by the first report to arrive.
property int frameAtMove: -1
property int reports: 0
PhysicsWorld {
id: world
gravity: Qt.vector3d(0, -981, 0)
running: true
typicalLength: 100
typicalSpeed: 1000
minimumTimestep: 15
maximumTimestep: 15
scene: fallingScene
property int frameCount: 0
onFrameDone: world.frameCount++
}
Node {
id: fallingScene
TriggerBody {
id: gate
collisionShapes: BoxShape {
extents: Qt.vector3d(400, 100, 100)
}
onBodyEntered: (body) => {
root.reports++;
if (root.frameAtMove < 0) {
root.frameAtMove = world.frameCount;
world.scene = elsewhere;
}
}
}
// Side by side and let go of from the same height, so they reach the
// trigger on the same step and the pairs that say so arrive together.
Repeater3D {
model: 3
DynamicRigidBody {
required property int index
position: Qt.vector3d(index * 120 - 120, 200, 0)
sendTriggerReports: true
collisionShapes: BoxShape {
extents: Qt.vector3d(50, 50, 50)
}
}
}
}
// What the world is moved to. It holds no bodies, so the move leaves every
// body without a world rather than handing it to a new one.
Node {
id: elsewhere
}
PhysicsTestCase {
name: "moving the world from a trigger report reaches the next frame"
timeoutSecs: 10
goalReached: root.frameAtMove >= 0 && world.frameCount > root.frameAtMove + 1
}
PhysicsTestCase {
name: "a body the move took out is not reported"
timeoutSecs: 10
goalReached: root.frameAtMove >= 0 && world.frameCount > root.frameAtMove + 1
&& root.reports === 1
}
}