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 while it is walking its bodies to sync
// them. A handler on the position a sync writes moves the world to another
// scene, which deregisters every body at once, so each body the walk has not
// reached yet is left with nothing to sync. Every body carries the handler, so
// whichever the walk reaches first does the moving and the ones behind it are
// the ones left behind. Syncing them crashed.
//
// Nothing is deleted here, and that is the point: a scene change leaves a
// backend without a frontend just as a delete does, and it lands while the walk
// is still on the stack, rather than whenever the object a delete was asked for
// is deferred to.
//
// Reaching the end of both of the frame's walks is the assertion, asked for as
// two more finished frames after the move.
//
// No rendering is needed for any of this, so scene is a plain Node rather than
// a View3D: debug drawing below builds its models against that node and nothing
// asks to see them.
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 whichever body the walk reached
// first.
property int frameAtMove: -1
PhysicsWorld {
id: world
gravity: Qt.vector3d(0, -981, 0)
running: true
// On, so the frame that loses its scene mid-walk goes on to the second
// walk it makes over the same backends, the one debug drawing does.
forceDebugDraw: true
typicalLength: 100
typicalSpeed: 1000
minimumTimestep: 15
maximumTimestep: 15
scene: fallingScene
property int frameCount: 0
onFrameDone: world.frameCount++
}
Node {
id: fallingScene
// Falling freely, so every sync has a new position to write and no body
// ever falls asleep and stops being written to.
Repeater3D {
model: 3
DynamicRigidBody {
required property int index
position: Qt.vector3d(index * 120 - 120, 200, 0)
collisionShapes: BoxShape {}
// Held off until the world has frames behind it, since setting
// the position above emits this too, and moving the world from
// there would move it before it ever walked anything.
onPositionChanged: {
if (world.frameCount > 5 && world.scene === fallingScene) {
root.frameAtMove = world.frameCount;
world.scene = elsewhere;
}
}
}
}
}
// 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 while it syncs its bodies"
goalReached: root.frameAtMove >= 0 && world.frameCount > root.frameAtMove + 1
}
}