Skip to content

Commit f8d4d12

Browse files
committed
Test AsArg when interacting with engine + custom GDScript
1 parent 9d2cbf2 commit f8d4d12

File tree

2 files changed

+209
-4
lines changed

2 files changed

+209
-4
lines changed

godot-core/src/meta/args/cow_arg.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ impl<T> Deref for CowArg<'_, T> {
228228
CowArg::Owned(value) => value,
229229
CowArg::Borrowed(value) => value,
230230
CowArg::FfiObject(_) => {
231-
todo!("Deref not implemented for FfiObject variant")
231+
unreachable!("deref(): FfiObject path should only be used for FFI logic")
232232
}
233233
}
234234
}

itest/rust/src/object_tests/object_arg_test.rs

Lines changed: 208 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8-
use godot::builtin::Variant;
9-
use godot::classes::{ClassDb, Node, ResourceFormatLoader, ResourceLoader};
8+
use godot::builtin::{vslice, Variant};
9+
use godot::classes::{ClassDb, Node, RefCounted, ResourceFormatLoader, ResourceLoader};
1010
use godot::global;
11+
use godot::meta::ToGodot;
1112
use godot::obj::{Gd, NewAlloc, NewGd};
13+
use godot::register::{godot_api, GodotClass};
1214

13-
use crate::framework::itest;
15+
use crate::framework::{create_gdscript, itest};
1416
use crate::object_tests::object_test::{user_refc_instance, RefcPayload};
1517

1618
/*
@@ -142,6 +144,73 @@ fn object_arg_owned_default_params() {
142144
ResourceLoader::singleton().remove_resource_format_loader(&b);
143145
}
144146

147+
// Gd<RefCounted> passed to GDScript should not create unnecessary clones.
148+
#[itest]
149+
fn refcount_asarg_gdscript_calls() {
150+
let script = create_gdscript(
151+
r#"
152+
extends RefCounted
153+
154+
func observe_refcount_ptrcall(obj: RefCounted) -> int:
155+
return obj.get_reference_count()
156+
157+
func observe_refcount_varcall(obj) -> int:
158+
if obj == null:
159+
return 0
160+
return obj.get_reference_count()
161+
"#,
162+
);
163+
164+
let mut test_instance = RefCounted::new_gd();
165+
test_instance.set_script(&script);
166+
167+
// Already pack into Variant, to have 1 less reference count increment.
168+
let refc = RefCounted::new_gd().to_variant();
169+
assert_eq!(refc.call("get_reference_count", &[]), 1.to_variant());
170+
171+
let refcount_typed: i32 = test_instance
172+
.call("observe_refcount_ptrcall", &[refc])
173+
.to::<i32>();
174+
175+
let refc = RefCounted::new_gd().to_variant();
176+
let refcount_untyped = test_instance
177+
.call("observe_refcount_varcall", &[refc])
178+
.to::<i32>();
179+
180+
let refcount_none = test_instance
181+
.call("observe_refcount_varcall", vslice![Variant::nil()])
182+
.to::<i32>();
183+
184+
// Both should result in refcount 2: 1 variant in Rust + 1 reference created on GDScript side.
185+
assert_eq!(refcount_typed, 2, "typed GDScript param (ptrcall)");
186+
assert_eq!(refcount_untyped, 2, "untyped GDScript param (varcall)");
187+
assert_eq!(refcount_none, 0, "None/null parameter should return 0");
188+
}
189+
190+
// ----------------------------------------------------------------------------------------------------------------------------------------------
191+
// Tests with engine APIs + AsArg below, in module.
192+
193+
#[derive(GodotClass)]
194+
#[class(base = RefCounted, init)]
195+
pub struct RefCountAsArgTest;
196+
197+
#[godot_api]
198+
impl RefCountAsArgTest {
199+
#[func]
200+
fn accept_option_refcounted(&self, obj: Option<Gd<RefCounted>>) -> i32 {
201+
match obj {
202+
Some(gd) => gd.get_reference_count(),
203+
None => 0,
204+
}
205+
}
206+
207+
#[func]
208+
fn accept_object(&self, obj: Gd<godot::classes::Object>) -> bool {
209+
// Just verify we can receive an Object (for upcast testing).
210+
!obj.get_class().is_empty()
211+
}
212+
}
213+
145214
// ----------------------------------------------------------------------------------------------------------------------------------------------
146215
// Helpers
147216

@@ -164,3 +233,139 @@ where
164233

165234
manual2.free();
166235
}
236+
237+
// ----------------------------------------------------------------------------------------------------------------------------------------------
238+
// Tests requiring codegen-full feature
239+
240+
#[cfg(feature = "codegen-full")]
241+
mod engine_api_tests {
242+
use std::cell::Cell;
243+
244+
use godot::builtin::{Rid, Variant};
245+
use godot::classes::{base_material_3d, ITexture2D, StandardMaterial3D, Texture2D};
246+
use godot::meta::ToGodot;
247+
use godot::obj::{Base, Gd, NewGd, WithBaseField};
248+
use godot::register::{godot_api, GodotClass};
249+
250+
use crate::framework::itest;
251+
252+
const ALBEDO: base_material_3d::TextureParam = base_material_3d::TextureParam::ALBEDO;
253+
254+
/// Various internal references are created during `set_texture()`, thus 4. This also matches GDScript code doing the same.
255+
/// Verified that before this optimization, the refcount was 5.
256+
const EXPECTED_REFCOUNT: i32 = 4;
257+
258+
fn verify_refcount<F>(exp_refcount: i32, arg_desription: &str, operation: F)
259+
where
260+
F: FnOnce(&mut Gd<StandardMaterial3D>, &Gd<ArgTestTexture>),
261+
{
262+
let texture = ArgTestTexture::new_gd();
263+
let mut material = StandardMaterial3D::new_gd();
264+
265+
operation(&mut material, &texture);
266+
267+
let captured = texture.bind().get_captured_refcount();
268+
assert_eq!(captured, exp_refcount, "{}", arg_desription);
269+
}
270+
271+
#[itest]
272+
fn refcount_asarg_ref() {
273+
verify_refcount(EXPECTED_REFCOUNT, "&object", |mat, tex| {
274+
// Sanity check: refcount is 1 before call.
275+
assert_eq!(tex.get_reference_count(), 1);
276+
277+
mat.set_texture(ALBEDO, tex);
278+
});
279+
280+
// Derived -> base conversion. 1 extra due to clone().
281+
verify_refcount(EXPECTED_REFCOUNT + 1, "&base_obj", |mat, tex| {
282+
let base = tex.clone().upcast::<Texture2D>();
283+
mat.set_texture(ALBEDO, &base);
284+
});
285+
}
286+
287+
#[itest]
288+
fn refcount_asarg_option() {
289+
verify_refcount(EXPECTED_REFCOUNT, "Some(&object)", |mat, tex| {
290+
mat.set_texture(ALBEDO, Some(tex));
291+
});
292+
293+
// Derived -> base conversion. 1 extra due to clone().
294+
verify_refcount(EXPECTED_REFCOUNT + 1, "Some(&base_obj)", |mat, tex| {
295+
let base = tex.clone().upcast::<Texture2D>();
296+
mat.set_texture(ALBEDO, Some(&base));
297+
});
298+
299+
verify_refcount(0, "None [derived]", |mat, _tex| {
300+
mat.set_texture(ALBEDO, None::<&Gd<ArgTestTexture>>);
301+
});
302+
303+
verify_refcount(0, "None [base]", |mat, _tex| {
304+
mat.set_texture(ALBEDO, None::<&Gd<Texture2D>>);
305+
});
306+
}
307+
308+
#[itest]
309+
fn refcount_asarg_null_arg() {
310+
verify_refcount(0, "Gd::null_arg()", |mat, _tex| {
311+
mat.set_texture(ALBEDO, Gd::null_arg());
312+
});
313+
}
314+
315+
#[itest]
316+
fn refcount_asarg_variant() {
317+
verify_refcount(EXPECTED_REFCOUNT, "&Variant(tex)", |mat, tex| {
318+
mat.set("albedo_texture", &tex.to_variant());
319+
});
320+
321+
verify_refcount(0, "&Variant(nil)", |mat, _tex| {
322+
mat.set("albedo_texture", &Variant::nil());
323+
});
324+
}
325+
326+
// ------------------------------------------------------------------------------------------------------------------------------------------
327+
// Test classes for AsArg testing
328+
329+
#[derive(GodotClass)]
330+
#[class(base = Texture2D)]
331+
pub struct ArgTestTexture {
332+
base: Base<Texture2D>,
333+
captured_refcount: Cell<i32>,
334+
rid: Rid,
335+
}
336+
337+
#[godot_api]
338+
impl ArgTestTexture {
339+
fn get_captured_refcount(&self) -> i32 {
340+
self.captured_refcount.get()
341+
}
342+
}
343+
344+
#[godot_api]
345+
impl ITexture2D for ArgTestTexture {
346+
fn init(base: Base<Texture2D>) -> Self {
347+
Self {
348+
base,
349+
captured_refcount: Cell::new(0),
350+
rid: Rid::new(0),
351+
}
352+
}
353+
354+
// Override this method because it's called by StandardMaterial3D::set_texture().
355+
// We use it as a hook to observe the reference count from the engine side (after passing through AsArg).
356+
fn get_rid(&self) -> Rid {
357+
self.captured_refcount
358+
.set(self.base().get_reference_count());
359+
360+
self.rid
361+
}
362+
363+
fn get_width(&self) -> i32 {
364+
1
365+
}
366+
367+
fn get_height(&self) -> i32 {
368+
1
369+
}
370+
}
371+
}

0 commit comments

Comments
 (0)