Skip to content

Commit 6a47e9b

Browse files
committed
Implement support for RID
Add `RenderingServer` to minimal classes, to be able to test the RID impl against an actual server Add `Engine` to minimal classes, to be able to disable error printing in itests
1 parent 54cb201 commit 6a47e9b

File tree

7 files changed

+238
-3
lines changed

7 files changed

+238
-3
lines changed

godot-codegen/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ const SELECTED_CLASSES: &[&str] = &[
268268
"CollisionObject2D",
269269
"CollisionShape2D",
270270
"Control",
271+
"Engine",
271272
"FileAccess",
272273
"HTTPRequest",
273274
"Image",
@@ -286,6 +287,7 @@ const SELECTED_CLASSES: &[&str] = &[
286287
"PathFollow2D",
287288
"PhysicsBody2D",
288289
"RefCounted",
290+
"RenderingServer",
289291
"Resource",
290292
"ResourceLoader",
291293
"RigidBody2D",

godot-core/src/builtin/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ pub use others::*;
4545
pub use packed_array::*;
4646
pub use projection::*;
4747
pub use quaternion::*;
48+
pub use rid::*;
4849
pub use string::*;
4950
pub use string_name::*;
5051
pub use transform2d::*;
@@ -92,6 +93,7 @@ mod others;
9293
mod packed_array;
9394
mod projection;
9495
mod quaternion;
96+
mod rid;
9597
mod string;
9698
mod string_name;
9799
mod transform2d;

godot-core/src/builtin/others.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ impl_builtin_stub!(Rect2, OpaqueRect2);
1717
impl_builtin_stub!(Rect2i, OpaqueRect2i);
1818
impl_builtin_stub!(Plane, OpaquePlane);
1919
impl_builtin_stub!(Aabb, OpaqueAabb);
20-
impl_builtin_stub!(Rid, OpaqueRid);
2120
impl_builtin_stub!(Callable, OpaqueCallable);
2221
impl_builtin_stub!(Signal, OpaqueSignal);
2322

godot-core/src/builtin/rid.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
use std::num::NonZeroU64;
8+
9+
use godot_ffi as sys;
10+
use sys::{ffi_methods, GodotFfi};
11+
12+
/// A RID ("resource ID") is an opaque handle that refers to a Godot `Resource`.
13+
///
14+
/// RIDs do not grant access to the resource itself. Instead, they can be used in lower-level resource APIs
15+
/// such as the [servers]. See also [Godot API docs for `RID`][docs].
16+
///
17+
/// RIDs should be largely safe to work with. Certain calls to servers may fail, however doing so will
18+
/// trigger an error from Godot, and will not cause any UB.
19+
///
20+
/// # Safety Caveat:
21+
///
22+
/// In Godot 3, RID was not as safe as described here. We believe that this is fixed in Godot 4, but this has
23+
/// not been extensively tested as of yet. Some confirmed UB from Godot 3 does not occur anymore, but if you
24+
/// find anything suspicious or outright UB please open an issue.
25+
///
26+
/// [servers]: https://docs.godotengine.org/en/stable/tutorials/optimization/using_servers.html
27+
/// [docs]: https://docs.godotengine.org/en/stable/classes/class_rid.html
28+
29+
// Using normal rust repr to take advantage advantage of the nullable pointer optimization. As this enum is
30+
// eligible for it, it is also guaranteed to have it. Meaning the layout of this type is identical to `u64`.
31+
// See: https://doc.rust-lang.org/nomicon/ffi.html#the-nullable-pointer-optimization
32+
// Cannot use `#[repr(C)]` as it does not use the nullable pointer optimization.
33+
#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Debug)]
34+
pub enum Rid {
35+
/// A valid RID may refer to some resource, but is not guaranteed to do so.
36+
Valid(NonZeroU64),
37+
/// An invalid RID will never refer to a resource. Internally it is represented as a 0.
38+
Invalid,
39+
}
40+
41+
impl Rid {
42+
/// Create a new RID.
43+
#[inline]
44+
pub const fn new(id: u64) -> Self {
45+
match NonZeroU64::new(id) {
46+
Some(id) => Self::Valid(id),
47+
None => Self::Invalid,
48+
}
49+
}
50+
51+
/// Convert this RID into a [`u64`]. Returns 0 if it is invalid.
52+
///
53+
/// _Godot equivalent: `Rid.get_id()`_
54+
#[inline]
55+
pub const fn to_u64(self) -> u64 {
56+
match self {
57+
Rid::Valid(id) => id.get(),
58+
Rid::Invalid => 0,
59+
}
60+
}
61+
62+
/// Convert this RID into a [`u64`] if it is valid. Otherwise return None.
63+
#[inline]
64+
pub const fn to_valid_u64(self) -> Option<u64> {
65+
match self {
66+
Rid::Valid(id) => Some(id.get()),
67+
Rid::Invalid => None,
68+
}
69+
}
70+
71+
/// Convert this RID into a [`NonZeroU64`].
72+
#[inline]
73+
pub const fn to_non_zero_u64(self) -> Option<NonZeroU64> {
74+
match self {
75+
Rid::Valid(id) => Some(id),
76+
Rid::Invalid => None,
77+
}
78+
}
79+
80+
/// Returns `true` if this is a valid RID.
81+
#[inline]
82+
pub const fn is_valid(&self) -> bool {
83+
matches!(self, Rid::Valid(_))
84+
}
85+
86+
/// Returns `true` if this is an invalid RID.
87+
#[inline]
88+
pub const fn is_invalid(&self) -> bool {
89+
matches!(self, Rid::Invalid)
90+
}
91+
}
92+
93+
impl GodotFfi for Rid {
94+
ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. }
95+
}

godot-core/src/builtin/variant/impls.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ mod impls {
159159
impl_variant_metadata!(Rect2i, /* rect2i_to_variant, rect2i_from_variant, */ Rect2i);
160160
impl_variant_metadata!(Plane, /* plane_to_variant, plane_from_variant, */ Plane);
161161
impl_variant_metadata!(Aabb, /* aabb_to_variant, aabb_from_variant, */ Aabb);
162-
impl_variant_metadata!(Rid, /* rid_to_variant, rid_from_variant, */ Rid);
163162
impl_variant_metadata!(Callable, /* callable_to_variant, callable_from_variant, */ Callable);
164163
impl_variant_metadata!(Signal, /* signal_to_variant, signal_from_variant, */ Signal);
165164
impl_variant_traits!(PackedByteArray, packed_byte_array_to_variant, packed_byte_array_from_variant, PackedByteArray);
@@ -172,6 +171,7 @@ mod impls {
172171
impl_variant_traits!(PackedVector3Array, packed_vector3_array_to_variant, packed_vector3_array_from_variant, PackedVector3Array);
173172
impl_variant_traits!(PackedColorArray, packed_color_array_to_variant, packed_color_array_from_variant, PackedColorArray);
174173
impl_variant_traits!(Projection, projection_to_variant, projection_from_variant, Projection);
174+
impl_variant_traits!(Rid, rid_to_variant, rid_from_variant, Rid);
175175
impl_variant_traits!(Transform2D, transform_2d_to_variant, transform_2d_from_variant, Transform2D);
176176
impl_variant_traits!(Transform3D, transform_3d_to_variant, transform_3d_from_variant, Transform3D);
177177
impl_variant_traits!(Dictionary, dictionary_to_variant, dictionary_from_variant, Dictionary);

itest/rust/src/lib.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
55
*/
66

7-
use godot::engine::Node;
7+
use godot::engine::{Engine, Node};
88
use godot::init::{gdextension, ExtensionLibrary};
99
use godot::obj::Gd;
1010
use godot::sys;
@@ -23,6 +23,7 @@ mod node_test;
2323
mod object_test;
2424
mod packed_array_test;
2525
mod quaternion_test;
26+
mod rid_test;
2627
mod singleton_test;
2728
mod string_test;
2829
mod transform2d_test;
@@ -55,6 +56,15 @@ pub(crate) fn expect_panic(context: &str, code: impl FnOnce() + std::panic::Unwi
5556
);
5657
}
5758

59+
/// Disable printing errors from Godot. Ideally we should catch and handle errors, ensuring they happen when
60+
/// expected. But that isn't possible, so for now we can just disable printing the error to avoid spamming
61+
/// the terminal when tests should error.
62+
pub(crate) fn suppress_godot_print(mut f: impl FnMut()) {
63+
Engine::singleton().set_print_error_messages(false);
64+
f();
65+
Engine::singleton().set_print_error_messages(true);
66+
}
67+
5868
// ----------------------------------------------------------------------------------------------------------------------------------------------
5969
// Entry point + #[itest] test registration
6070

itest/rust/src/rid_test.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
use std::{collections::HashSet, thread};
8+
9+
use godot::{
10+
engine::RenderingServer,
11+
prelude::{inner::InnerRid, Color, Rid, Vector2},
12+
};
13+
14+
use crate::{itest, suppress_godot_print};
15+
16+
#[itest]
17+
fn rid_equiv() {
18+
let invalid: Rid = Rid::Invalid;
19+
let valid: Rid = Rid::new((10 << 32) | 20);
20+
assert!(!InnerRid::from_outer(&invalid).is_valid());
21+
assert!(InnerRid::from_outer(&valid).is_valid());
22+
23+
assert_eq!(InnerRid::from_outer(&invalid).get_id(), 0);
24+
assert_eq!(InnerRid::from_outer(&valid).get_id(), (10 << 32) | 20);
25+
}
26+
27+
#[itest]
28+
fn canvas_set_parent() {
29+
// This originally caused UB, but still testing it here in case it breaks.
30+
let mut server = RenderingServer::singleton();
31+
let canvas = server.canvas_create();
32+
let viewport = server.viewport_create();
33+
34+
suppress_godot_print(|| server.canvas_item_set_parent(viewport, canvas));
35+
suppress_godot_print(|| server.canvas_item_set_parent(viewport, viewport));
36+
37+
server.free_rid(canvas);
38+
server.free_rid(viewport);
39+
}
40+
41+
#[itest]
42+
fn multi_thread_test() {
43+
let threads = (0..10)
44+
.map(|_| {
45+
thread::spawn(|| {
46+
let mut server = RenderingServer::singleton();
47+
(0..1000).map(|_| server.canvas_item_create()).collect()
48+
})
49+
})
50+
.collect::<Vec<_>>();
51+
52+
let mut rids: Vec<Rid> = vec![];
53+
54+
for thread in threads.into_iter() {
55+
rids.append(&mut thread.join().unwrap());
56+
}
57+
58+
let set = rids.iter().cloned().collect::<HashSet<_>>();
59+
assert_eq!(set.len(), rids.len());
60+
61+
let mut server = RenderingServer::singleton();
62+
63+
for rid in rids.iter() {
64+
server.canvas_item_add_circle(*rid, Vector2::ZERO, 1.0, Color::from_rgb(1.0, 0.0, 0.0));
65+
}
66+
67+
for rid in rids.iter() {
68+
server.free_rid(*rid);
69+
}
70+
}
71+
72+
/// Check that godot does not crash upon receiving various RIDs that may be edge cases. As it could do in Godot 3.
73+
#[itest]
74+
fn strange_rids() {
75+
let mut server = RenderingServer::singleton();
76+
let mut rids: Vec<u64> = vec![
77+
// Invalid RID.
78+
0,
79+
// Normal RID, should work without issue.
80+
1,
81+
10,
82+
// Testing the boundaries of various ints.
83+
u8::MAX as u64,
84+
u16::MAX as u64,
85+
u32::MAX as u64,
86+
u64::MAX,
87+
i8::MIN as u64,
88+
i8::MAX as u64,
89+
i16::MIN as u64,
90+
i16::MAX as u64,
91+
i32::MIN as u64,
92+
i32::MAX as u64,
93+
i64::MIN as u64,
94+
i64::MAX as u64,
95+
// Biggest RIDs possible in Godot (ignoring local indices).
96+
0xFFFFFFFF << 32,
97+
0x7FFFFFFF << 32,
98+
// Godot's servers treats RIDs as two u32s, so testing what happens round the region where
99+
// one u32 overflows into the next.
100+
u32::MAX as u64 + 1,
101+
u32::MAX as u64 + 2,
102+
u32::MAX as u64 - 1,
103+
u32::MAX as u64 - 2,
104+
// A couple random RIDs.
105+
1234567891011121314,
106+
14930753991246632225,
107+
8079365198791785081,
108+
10737267678893224303,
109+
12442588258967011829,
110+
4275912429544145425,
111+
];
112+
// Checking every number with exactly 2 bits = 1.
113+
// An approximation of exhaustively checking every number.
114+
for i in 0..64 {
115+
for j in 0..63 {
116+
if j >= i {
117+
rids.push((1 << i) | (1 << (j + 1)))
118+
} else {
119+
rids.push((1 << i) | (1 << j))
120+
}
121+
}
122+
}
123+
124+
for id in rids.iter() {
125+
suppress_godot_print(|| server.canvas_item_clear(Rid::new(*id)))
126+
}
127+
}

0 commit comments

Comments
 (0)