Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/3d/occlusion_culling.rs
9354 views
1
//! Demonstrates occlusion culling.
2
//!
3
//! This demo rotates many small cubes around a rotating large cube at the
4
//! origin. At all times, the large cube will be occluding several of the small
5
//! cubes. The demo displays the number of cubes that were actually rendered, so
6
//! the effects of occlusion culling can be seen.
7
8
use std::{
9
any::TypeId,
10
f32::consts::PI,
11
fmt::Write as _,
12
sync::{Arc, Mutex},
13
};
14
15
use bevy::{
16
color::palettes::css::{SILVER, WHITE},
17
core_pipeline::{core_3d::Opaque3d, prepass::DepthPrepass, Core3d, Core3dSystems},
18
pbr::PbrPlugin,
19
prelude::*,
20
render::{
21
batching::gpu_preprocessing::{
22
GpuPreprocessingSupport, IndirectParametersBuffers, IndirectParametersIndexed,
23
},
24
occlusion_culling::OcclusionCulling,
25
render_resource::{Buffer, BufferDescriptor, BufferUsages, MapMode},
26
renderer::{RenderContext, RenderDevice},
27
settings::WgpuFeatures,
28
Render, RenderApp, RenderDebugFlags, RenderPlugin, RenderStartup, RenderSystems,
29
},
30
};
31
use bytemuck::Pod;
32
33
/// The radius of the spinning sphere of cubes.
34
const OUTER_RADIUS: f32 = 3.0;
35
36
/// The density of cubes in the other sphere.
37
const OUTER_SUBDIVISION_COUNT: u32 = 5;
38
39
/// The speed at which the outer sphere and large cube rotate in radians per
40
/// frame.
41
const ROTATION_SPEED: f32 = 0.01;
42
43
/// The length of each side of the small cubes, in meters.
44
const SMALL_CUBE_SIZE: f32 = 0.1;
45
46
/// The length of each side of the large cube, in meters.
47
const LARGE_CUBE_SIZE: f32 = 2.0;
48
49
/// A marker component for the immediate parent of the large sphere of cubes.
50
#[derive(Default, Component)]
51
struct SphereParent;
52
53
/// A marker component for the large spinning cube at the origin.
54
#[derive(Default, Component)]
55
struct LargeCube;
56
57
/// A plugin for the render app that reads the number of culled meshes from the
58
/// GPU back to the CPU.
59
struct ReadbackIndirectParametersPlugin;
60
61
/// The intermediate staging buffers that we use to read back the indirect
62
/// parameters from the GPU to the CPU.
63
///
64
/// We read back the GPU indirect parameters so that we can determine the number
65
/// of meshes that were culled.
66
///
67
/// `wgpu` doesn't allow us to read indirect buffers back from the GPU to the
68
/// CPU directly. Instead, we have to copy them to a temporary staging buffer
69
/// first, and then read *those* buffers back from the GPU to the CPU. This
70
/// resource holds those temporary buffers.
71
#[derive(Resource, Default)]
72
struct IndirectParametersStagingBuffers {
73
/// The buffer that stores the indirect draw commands.
74
///
75
/// See [`IndirectParametersIndexed`] for more information about the memory
76
/// layout of this buffer.
77
data: Option<Buffer>,
78
/// The buffer that stores the *number* of indirect draw commands.
79
///
80
/// We only care about the first `u32` in this buffer.
81
batch_sets: Option<Buffer>,
82
}
83
84
/// A resource, shared between the main world and the render world, that saves a
85
/// CPU-side copy of the GPU buffer that stores the indirect draw parameters.
86
///
87
/// This is needed so that we can display the number of meshes that were culled.
88
/// It's reference counted, and protected by a lock, because we don't precisely
89
/// know when the GPU will be ready to present the CPU with the buffer copy.
90
/// Even though the rendering runs at least a frame ahead of the main app logic,
91
/// we don't require more precise synchronization than the lock because we don't
92
/// really care how up-to-date the counter of culled meshes is. If it's off by a
93
/// few frames, that's no big deal.
94
#[derive(Clone, Resource, Deref, DerefMut)]
95
struct SavedIndirectParameters(Arc<Mutex<Option<SavedIndirectParametersData>>>);
96
97
/// A CPU-side copy of the GPU buffer that stores the indirect draw parameters.
98
///
99
/// This is needed so that we can display the number of meshes that were culled.
100
struct SavedIndirectParametersData {
101
/// The CPU-side copy of the GPU buffer that stores the indirect draw
102
/// parameters.
103
data: Vec<IndirectParametersIndexed>,
104
/// The CPU-side copy of the GPU buffer that stores the *number* of indirect
105
/// draw parameters that we have.
106
///
107
/// All we care about is the number of indirect draw parameters for a single
108
/// view, so this is only one word in size.
109
count: u32,
110
/// True if occlusion culling is supported at all; false if it's not.
111
occlusion_culling_supported: bool,
112
/// True if we support inspecting the number of meshes that were culled on
113
/// this platform; false if we don't.
114
///
115
/// If `multi_draw_indirect_count` isn't supported, then we would have to
116
/// employ a more complicated approach in order to determine the number of
117
/// meshes that are occluded, and that would be out of scope for this
118
/// example.
119
occlusion_culling_introspection_supported: bool,
120
}
121
122
impl SavedIndirectParameters {
123
fn new() -> Self {
124
Self(Arc::new(Mutex::new(None)))
125
}
126
}
127
128
fn init_saved_indirect_parameters(
129
render_device: Res<RenderDevice>,
130
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
131
saved_indirect_parameters: Res<SavedIndirectParameters>,
132
) {
133
let mut saved_indirect_parameters = saved_indirect_parameters.0.lock().unwrap();
134
*saved_indirect_parameters = Some(SavedIndirectParametersData {
135
data: vec![],
136
count: 0,
137
occlusion_culling_supported: gpu_preprocessing_support.is_culling_supported(),
138
// In order to determine how many meshes were culled, we look at the indirect count buffer
139
// that Bevy only populates if the platform supports `multi_draw_indirect_count`. So, if we
140
// don't have that feature, then we don't bother to display how many meshes were culled.
141
occlusion_culling_introspection_supported: render_device
142
.features()
143
.contains(WgpuFeatures::MULTI_DRAW_INDIRECT_COUNT),
144
});
145
}
146
147
/// The demo's current settings.
148
#[derive(Resource)]
149
struct AppStatus {
150
/// Whether occlusion culling is presently enabled.
151
///
152
/// By default, this is set to true.
153
occlusion_culling: bool,
154
}
155
156
impl Default for AppStatus {
157
fn default() -> Self {
158
AppStatus {
159
occlusion_culling: true,
160
}
161
}
162
}
163
164
fn main() {
165
let render_debug_flags = RenderDebugFlags::ALLOW_COPIES_FROM_INDIRECT_PARAMETERS;
166
167
App::new()
168
.add_plugins(
169
DefaultPlugins
170
.set(WindowPlugin {
171
primary_window: Some(Window {
172
title: "Bevy Occlusion Culling Example".into(),
173
..default()
174
}),
175
..default()
176
})
177
.set(RenderPlugin {
178
debug_flags: render_debug_flags,
179
..default()
180
})
181
.set(PbrPlugin {
182
debug_flags: render_debug_flags,
183
..default()
184
}),
185
)
186
.add_plugins(ReadbackIndirectParametersPlugin)
187
.init_resource::<AppStatus>()
188
.add_systems(Startup, setup)
189
.add_systems(Update, spin_small_cubes)
190
.add_systems(Update, spin_large_cube)
191
.add_systems(Update, update_status_text)
192
.add_systems(Update, toggle_occlusion_culling_on_request)
193
.run();
194
}
195
196
impl Plugin for ReadbackIndirectParametersPlugin {
197
fn build(&self, app: &mut App) {
198
// Create the `SavedIndirectParameters` resource that we're going to use
199
// to communicate between the thread that the GPU-to-CPU readback
200
// callback runs on and the main application threads. This resource is
201
// atomically reference counted. We store one reference to the
202
// `SavedIndirectParameters` in the main app and another reference in
203
// the render app.
204
let saved_indirect_parameters = SavedIndirectParameters::new();
205
app.insert_resource(saved_indirect_parameters.clone());
206
207
// Fetch the render app.
208
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
209
return;
210
};
211
212
render_app
213
// Insert another reference to the `SavedIndirectParameters`.
214
.insert_resource(saved_indirect_parameters)
215
// Setup the parameters in RenderStartup.
216
.add_systems(RenderStartup, init_saved_indirect_parameters)
217
.init_resource::<IndirectParametersStagingBuffers>()
218
.add_systems(ExtractSchedule, readback_indirect_parameters)
219
.add_systems(
220
Render,
221
create_indirect_parameters_staging_buffers
222
.in_set(RenderSystems::PrepareResourcesFlush),
223
)
224
.add_systems(
225
Core3d,
226
// Add the node that allows us to read the indirect parameters back
227
// from the GPU to the CPU, which allows us to determine how many
228
// meshes were culled.
229
readback_indirect_parameters_node
230
// We read back the indirect parameters any time after
231
// `MainPass`. Readback doesn't particularly need to execute
232
// before PostProcess, but we order it that way anyway.
233
.after(Core3dSystems::MainPass)
234
.before(Core3dSystems::PostProcess),
235
);
236
}
237
}
238
239
/// Spawns all the objects in the scene.
240
fn setup(
241
mut commands: Commands,
242
asset_server: Res<AssetServer>,
243
mut meshes: ResMut<Assets<Mesh>>,
244
mut materials: ResMut<Assets<StandardMaterial>>,
245
) {
246
spawn_small_cubes(&mut commands, &mut meshes, &mut materials);
247
spawn_large_cube(&mut commands, &asset_server, &mut meshes, &mut materials);
248
spawn_light(&mut commands);
249
spawn_camera(&mut commands);
250
spawn_help_text(&mut commands);
251
}
252
253
/// Spawns the rotating sphere of small cubes.
254
fn spawn_small_cubes(
255
commands: &mut Commands,
256
meshes: &mut Assets<Mesh>,
257
materials: &mut Assets<StandardMaterial>,
258
) {
259
// Add the cube mesh.
260
let small_cube = meshes.add(Cuboid::new(
261
SMALL_CUBE_SIZE,
262
SMALL_CUBE_SIZE,
263
SMALL_CUBE_SIZE,
264
));
265
266
// Add the cube material.
267
let small_cube_material = materials.add(StandardMaterial {
268
base_color: SILVER.into(),
269
..default()
270
});
271
272
// Create the entity that the small cubes will be parented to. This is the
273
// entity that we rotate.
274
let sphere_parent = commands
275
.spawn(Transform::from_translation(Vec3::ZERO))
276
.insert(Visibility::default())
277
.insert(SphereParent)
278
.id();
279
280
// Now we have to figure out where to place the cubes. To do that, we create
281
// a sphere mesh, but we don't add it to the scene. Instead, we inspect the
282
// sphere mesh to find the positions of its vertices, and spawn a small cube
283
// at each one. That way, we end up with a bunch of cubes arranged in a
284
// spherical shape.
285
286
// Create the sphere mesh, and extract the positions of its vertices.
287
let sphere = Sphere::new(OUTER_RADIUS)
288
.mesh()
289
.ico(OUTER_SUBDIVISION_COUNT)
290
.unwrap();
291
let sphere_positions = sphere.attribute(Mesh::ATTRIBUTE_POSITION).unwrap();
292
293
// At each vertex, create a small cube.
294
for sphere_position in sphere_positions.as_float3().unwrap() {
295
let sphere_position = Vec3::from_slice(sphere_position);
296
let small_cube = commands
297
.spawn(Mesh3d(small_cube.clone()))
298
.insert(MeshMaterial3d(small_cube_material.clone()))
299
.insert(Transform::from_translation(sphere_position))
300
.id();
301
commands.entity(sphere_parent).add_child(small_cube);
302
}
303
}
304
305
/// Spawns the large cube at the center of the screen.
306
///
307
/// This cube rotates chaotically and occludes small cubes behind it.
308
fn spawn_large_cube(
309
commands: &mut Commands,
310
asset_server: &AssetServer,
311
meshes: &mut Assets<Mesh>,
312
materials: &mut Assets<StandardMaterial>,
313
) {
314
commands
315
.spawn(Mesh3d(meshes.add(Cuboid::new(
316
LARGE_CUBE_SIZE,
317
LARGE_CUBE_SIZE,
318
LARGE_CUBE_SIZE,
319
))))
320
.insert(MeshMaterial3d(materials.add(StandardMaterial {
321
base_color: WHITE.into(),
322
base_color_texture: Some(asset_server.load("branding/icon.png")),
323
..default()
324
})))
325
.insert(Transform::IDENTITY)
326
.insert(LargeCube);
327
}
328
329
// Spins the outer sphere a bit every frame.
330
//
331
// This ensures that the set of cubes that are hidden and shown varies over
332
// time.
333
fn spin_small_cubes(mut sphere_parents: Query<&mut Transform, With<SphereParent>>) {
334
for mut sphere_parent_transform in &mut sphere_parents {
335
sphere_parent_transform.rotate_y(ROTATION_SPEED);
336
}
337
}
338
339
/// Spins the large cube a bit every frame.
340
///
341
/// The chaotic rotation adds a bit of randomness to the scene to better
342
/// demonstrate the dynamicity of the occlusion culling.
343
fn spin_large_cube(mut large_cubes: Query<&mut Transform, With<LargeCube>>) {
344
for mut transform in &mut large_cubes {
345
transform.rotate(Quat::from_euler(
346
EulerRot::XYZ,
347
0.13 * ROTATION_SPEED,
348
0.29 * ROTATION_SPEED,
349
0.35 * ROTATION_SPEED,
350
));
351
}
352
}
353
354
/// Spawns a directional light to illuminate the scene.
355
fn spawn_light(commands: &mut Commands) {
356
commands
357
.spawn(DirectionalLight::default())
358
.insert(Transform::from_rotation(Quat::from_euler(
359
EulerRot::ZYX,
360
0.0,
361
PI * -0.15,
362
PI * -0.15,
363
)));
364
}
365
366
/// Spawns a camera that includes the depth prepass and occlusion culling.
367
fn spawn_camera(commands: &mut Commands) {
368
commands
369
.spawn(Camera3d::default())
370
.insert(Transform::from_xyz(0.0, 0.0, 9.0).looking_at(Vec3::ZERO, Vec3::Y))
371
.insert(DepthPrepass)
372
.insert(OcclusionCulling);
373
}
374
375
/// Spawns the help text at the upper left of the screen.
376
fn spawn_help_text(commands: &mut Commands) {
377
commands.spawn((
378
Text::new(""),
379
Node {
380
position_type: PositionType::Absolute,
381
top: px(12),
382
left: px(12),
383
..default()
384
},
385
));
386
}
387
388
fn readback_indirect_parameters_node(
389
mut render_context: RenderContext,
390
indirect_parameters_buffers: Res<IndirectParametersBuffers>,
391
indirect_parameters_mapping_buffers: Res<IndirectParametersStagingBuffers>,
392
) {
393
// Get the indirect parameters buffers corresponding to the opaque 3D
394
// phase, since all our meshes are in that phase.
395
let Some(phase_indirect_parameters_buffers) =
396
indirect_parameters_buffers.get(&TypeId::of::<Opaque3d>())
397
else {
398
return;
399
};
400
401
// Grab both the buffers we're copying from and the staging buffers
402
// we're copying to. Remember that we can't map the indirect parameters
403
// buffers directly, so we have to copy their contents to a staging
404
// buffer.
405
let (
406
Some(indexed_data_buffer),
407
Some(indexed_batch_sets_buffer),
408
Some(indirect_parameters_staging_data_buffer),
409
Some(indirect_parameters_staging_batch_sets_buffer),
410
) = (
411
phase_indirect_parameters_buffers.indexed.data_buffer(),
412
phase_indirect_parameters_buffers
413
.indexed
414
.batch_sets_buffer(),
415
indirect_parameters_mapping_buffers.data.as_ref(),
416
indirect_parameters_mapping_buffers.batch_sets.as_ref(),
417
)
418
else {
419
return;
420
};
421
422
// Copy from the indirect parameters buffers to the staging buffers.
423
render_context.command_encoder().copy_buffer_to_buffer(
424
indexed_data_buffer,
425
0,
426
indirect_parameters_staging_data_buffer,
427
0,
428
indexed_data_buffer.size(),
429
);
430
render_context.command_encoder().copy_buffer_to_buffer(
431
indexed_batch_sets_buffer,
432
0,
433
indirect_parameters_staging_batch_sets_buffer,
434
0,
435
indexed_batch_sets_buffer.size(),
436
);
437
}
438
439
/// Creates the staging buffers that we use to read back the indirect parameters
440
/// from the GPU to the CPU.
441
///
442
/// We read the indirect parameters from the GPU to the CPU in order to display
443
/// the number of meshes that were culled each frame.
444
///
445
/// We need these staging buffers because `wgpu` doesn't allow us to read the
446
/// contents of the indirect parameters buffers directly. We must first copy
447
/// them from the GPU to a staging buffer, and then read the staging buffer.
448
fn create_indirect_parameters_staging_buffers(
449
mut indirect_parameters_staging_buffers: ResMut<IndirectParametersStagingBuffers>,
450
indirect_parameters_buffers: Res<IndirectParametersBuffers>,
451
render_device: Res<RenderDevice>,
452
) {
453
let Some(phase_indirect_parameters_buffers) =
454
indirect_parameters_buffers.get(&TypeId::of::<Opaque3d>())
455
else {
456
return;
457
};
458
459
// Fetch the indirect parameters buffers that we're going to copy from.
460
let (Some(indexed_data_buffer), Some(indexed_batch_set_buffer)) = (
461
phase_indirect_parameters_buffers.indexed.data_buffer(),
462
phase_indirect_parameters_buffers
463
.indexed
464
.batch_sets_buffer(),
465
) else {
466
return;
467
};
468
469
// Build the staging buffers. Make sure they have the same sizes as the
470
// buffers we're copying from.
471
indirect_parameters_staging_buffers.data =
472
Some(render_device.create_buffer(&BufferDescriptor {
473
label: Some("indexed data staging buffer"),
474
size: indexed_data_buffer.size(),
475
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
476
mapped_at_creation: false,
477
}));
478
indirect_parameters_staging_buffers.batch_sets =
479
Some(render_device.create_buffer(&BufferDescriptor {
480
label: Some("indexed batch set staging buffer"),
481
size: indexed_batch_set_buffer.size(),
482
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
483
mapped_at_creation: false,
484
}));
485
}
486
487
/// Updates the app status text at the top of the screen.
488
fn update_status_text(
489
saved_indirect_parameters: Res<SavedIndirectParameters>,
490
mut texts: Query<&mut Text>,
491
meshes: Query<Entity, With<Mesh3d>>,
492
app_status: Res<AppStatus>,
493
) {
494
// How many meshes are in the scene?
495
let total_mesh_count = meshes.iter().count();
496
497
// Sample the rendered object count. Note that we don't synchronize beyond
498
// locking the data and therefore this will value will generally at least
499
// one frame behind. This is fine; this app is just a demonstration after
500
// all.
501
let (
502
rendered_object_count,
503
occlusion_culling_supported,
504
occlusion_culling_introspection_supported,
505
): (u32, bool, bool) = {
506
let saved_indirect_parameters = saved_indirect_parameters.lock().unwrap();
507
let Some(saved_indirect_parameters) = saved_indirect_parameters.as_ref() else {
508
// Bail out early if the resource isn't initialized yet.
509
return;
510
};
511
(
512
saved_indirect_parameters
513
.data
514
.iter()
515
.take(saved_indirect_parameters.count as usize)
516
.map(|indirect_parameters| indirect_parameters.instance_count)
517
.sum(),
518
saved_indirect_parameters.occlusion_culling_supported,
519
saved_indirect_parameters.occlusion_culling_introspection_supported,
520
)
521
};
522
523
// Change the text.
524
for mut text in &mut texts {
525
text.0 = String::new();
526
if !occlusion_culling_supported {
527
text.0
528
.push_str("Occlusion culling not supported on this platform");
529
continue;
530
}
531
532
let _ = writeln!(
533
&mut text.0,
534
"Occlusion culling {} (Press Space to toggle)",
535
if app_status.occlusion_culling {
536
"ON"
537
} else {
538
"OFF"
539
},
540
);
541
542
if !occlusion_culling_introspection_supported {
543
continue;
544
}
545
546
let _ = write!(
547
&mut text.0,
548
"{rendered_object_count}/{total_mesh_count} meshes rendered"
549
);
550
}
551
}
552
553
/// A system that reads the indirect parameters back from the GPU so that we can
554
/// report how many meshes were culled.
555
fn readback_indirect_parameters(
556
mut indirect_parameters_staging_buffers: ResMut<IndirectParametersStagingBuffers>,
557
saved_indirect_parameters: Res<SavedIndirectParameters>,
558
) {
559
// If culling isn't supported on this platform, bail.
560
if !saved_indirect_parameters
561
.lock()
562
.unwrap()
563
.as_ref()
564
.unwrap()
565
.occlusion_culling_supported
566
{
567
return;
568
}
569
570
// Grab the staging buffers.
571
let (Some(data_buffer), Some(batch_sets_buffer)) = (
572
indirect_parameters_staging_buffers.data.take(),
573
indirect_parameters_staging_buffers.batch_sets.take(),
574
) else {
575
return;
576
};
577
578
// Read the GPU buffers back.
579
let saved_indirect_parameters_0 = (**saved_indirect_parameters).clone();
580
let saved_indirect_parameters_1 = (**saved_indirect_parameters).clone();
581
readback_buffer::<IndirectParametersIndexed>(data_buffer, move |indirect_parameters| {
582
saved_indirect_parameters_0
583
.lock()
584
.unwrap()
585
.as_mut()
586
.unwrap()
587
.data = indirect_parameters.to_vec();
588
});
589
readback_buffer::<u32>(batch_sets_buffer, move |indirect_parameters_count| {
590
saved_indirect_parameters_1
591
.lock()
592
.unwrap()
593
.as_mut()
594
.unwrap()
595
.count = indirect_parameters_count[0];
596
});
597
}
598
599
// A helper function to asynchronously read an array of [`Pod`] values back from
600
// the GPU to the CPU.
601
//
602
// The given callback is invoked when the data is ready. The buffer will
603
// automatically be unmapped after the callback executes.
604
fn readback_buffer<T>(buffer: Buffer, callback: impl FnOnce(&[T]) + Send + 'static)
605
where
606
T: Pod,
607
{
608
// We need to make another reference to the buffer so that we can move the
609
// original reference into the closure below.
610
let original_buffer = buffer.clone();
611
original_buffer
612
.slice(..)
613
.map_async(MapMode::Read, move |result| {
614
// Make sure we succeeded.
615
if result.is_err() {
616
return;
617
}
618
619
{
620
// Cast the raw bytes in the GPU buffer to the appropriate type.
621
let buffer_view = buffer.slice(..).get_mapped_range();
622
let indirect_parameters: &[T] = bytemuck::cast_slice(
623
&buffer_view[0..(buffer_view.len() / size_of::<T>() * size_of::<T>())],
624
);
625
626
// Invoke the callback.
627
callback(indirect_parameters);
628
}
629
630
// Unmap the buffer. We have to do this before submitting any more
631
// GPU command buffers, or `wgpu` will assert.
632
buffer.unmap();
633
});
634
}
635
636
/// Adds or removes the [`OcclusionCulling`] and [`DepthPrepass`] components
637
/// when the user presses the spacebar.
638
fn toggle_occlusion_culling_on_request(
639
mut commands: Commands,
640
input: Res<ButtonInput<KeyCode>>,
641
mut app_status: ResMut<AppStatus>,
642
cameras: Query<Entity, With<Camera3d>>,
643
) {
644
// Only run when the user presses the spacebar.
645
if !input.just_pressed(KeyCode::Space) {
646
return;
647
}
648
649
// Toggle the occlusion culling flag in `AppStatus`.
650
app_status.occlusion_culling = !app_status.occlusion_culling;
651
652
// Add or remove the `OcclusionCulling` and `DepthPrepass` components as
653
// requested.
654
for camera in &cameras {
655
if app_status.occlusion_culling {
656
commands
657
.entity(camera)
658
.insert(DepthPrepass)
659
.insert(OcclusionCulling);
660
} else {
661
commands
662
.entity(camera)
663
.remove::<DepthPrepass>()
664
.remove::<OcclusionCulling>();
665
}
666
}
667
}
668
669