flo_render_canvas 0.3.1

Converts flo_canvas streams to flo_render streams
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
use super::matrix::*;
use super::resource_ids::*;
use super::render_entity::*;
use super::renderer_core::*;

use flo_canvas as canvas;
use flo_render as render;

use ::desync::*;

use futures::prelude::*;
use futures::task::{Context, Poll};
use futures::future::{BoxFuture};

use std::mem;
use std::pin::*;
use std::sync::*;

///
/// Tri-state version of 'option' that supports 'Unknown' as well as None and Some
///
#[derive(Clone, Copy, PartialEq)]
enum Maybe<T> {
    Unknown,
    None,
    Some(T)
}

///
/// Modifier to apply to the active shader
///
#[derive(Clone, PartialEq)]
enum ShaderModifier {
    /// The simple shader should be used
    Simple,

    /// Shader should use a dash pattern
    DashPattern(Vec<f32>),

    /// Shader should use a texture
    Texture(render::TextureId, render::Matrix, bool, f32),

    /// Shader should use a gradient
    Gradient(render::TextureId, render::Matrix, bool, f32),
}

///
/// Stream of rendering actions resulting from a draw instruction
///
pub struct RenderStream<'a> {
    /// The core where the render instructions are read from
    core: Arc<Desync<RenderCore>>,

    /// The ID of the buffer to use for rendering the background quad
    background_vertex_buffer: render::VertexBufferId,

    /// True if the frame is suspended (we're not going to generate any direct rendering due to this drawing operation)
    frame_suspended: bool,

    /// The future that is processing new drawing instructions
    processing_future: Option<BoxFuture<'a, ()>>,

    /// The current layer ID that we're processing
    layer_id: usize,

    /// The render entity within the layer that we're processing
    render_index: usize,

    /// Render actions waiting to be sent
    pending_stack: Vec<render::RenderAction>,

    /// The stack of operations to run when the rendering is complete (None if they've already been rendered)
    final_stack: Option<Vec<render::RenderAction>>,

    /// The transformation for the viewport
    viewport_transform: canvas::Transform2D
}

///
/// Represents the active state of the render stream
///
#[derive(Clone)]
struct RenderStreamState {
    /// The render target
    render_target: Option<render::RenderTargetId>,

    /// The blend mode to use
    blend_mode: Option<render::BlendMode>,

    /// The texture to use as the eraser mask (None for no eraser texture)
    erase_mask: Maybe<render::TextureId>,

    /// The texture to use for the clip mask (None for no clip mask)
    clip_mask: Maybe<render::TextureId>,

    /// The modifier to apply to the shader, if present
    shader_modifier: Option<ShaderModifier>,

    /// The transform to apply to the rendering instructions
    transform: Option<canvas::Transform2D>,

    /// The buffers to use to render the clipping region
    clip_buffers: Option<Vec<(render::VertexBufferId, render::IndexBufferId, usize)>>
}

impl<'a> RenderStream<'a> {
    ///
    /// Creates a new render stream
    ///
    pub fn new<ProcessFuture>(core: Arc<Desync<RenderCore>>, frame_suspended: bool, processing_future: ProcessFuture, viewport_transform: canvas::Transform2D, background_vertex_buffer: render::VertexBufferId, initial_action_stack: Vec<render::RenderAction>, final_action_stack: Vec<render::RenderAction>) -> RenderStream<'a>
    where   ProcessFuture: 'a+Send+Future<Output=()> {
        RenderStream {
            core:                       core,
            frame_suspended:            frame_suspended,
            background_vertex_buffer:   background_vertex_buffer,
            processing_future:          Some(processing_future.boxed()),
            pending_stack:              initial_action_stack,
            final_stack:                Some(final_action_stack),
            viewport_transform:         viewport_transform,
            layer_id:                   0,
            render_index:               0
        }
    }
}

impl<T> Maybe<T> {
    ///
    /// Converts to an optional value
    ///
    pub fn value(self) -> Option<Option<T>> {
        match self {
            Maybe::Unknown      => None,
            Maybe::None         => Some(None),
            Maybe::Some(val)    => Some(Some(val))
        }
    }
}

impl RenderStreamState {
    ///
    /// Creates a new render stream state
    ///
    fn new() -> RenderStreamState {
        RenderStreamState {
            render_target:      None,
            blend_mode:         None,
            erase_mask:         Maybe::Unknown,
            clip_mask:          Maybe::Unknown, 
            shader_modifier:    None,
            transform:          None,
            clip_buffers:       None
        }
    }

    ///
    /// Generates the actions required to set a particular dash pattern
    ///
    fn generate_dash_pattern(&self, pattern: &[f32]) -> Vec<render::RenderAction> {
        // Number of pixels in the dash pattern texture
        const DASH_WIDTH: usize = 256;

        // Total length determines how many bytes each dash uses
        let total_length: f32   = pattern.iter().cloned().sum();
        let pixel_length        = total_length / DASH_WIDTH as f32;

        // Do not generate a pattern for the case where the total length doesn't add up
        if total_length <= 0.0 {
            return vec![];
        }

        // Write the pixels for the dash pattern
        let mut pixels      = vec![];
        let mut pos         = 0.0;
        let mut col         = 255u8;
        let mut cur_pos     = pattern.iter();
        let mut dash_end    = *cur_pos.next().unwrap_or(&total_length);

        for _ in 0..DASH_WIDTH {
            // Switch colours while we're over the end of the dash position
            while dash_end < pos {
                let next_dash_len = cur_pos.next().unwrap_or(&total_length);
                col = if col == 0 { 255 } else { 0 };

                dash_end += next_dash_len;
            }

            // Write this pixel
            pixels.push(col);

            // Update the position
            pos += pixel_length;
        }

        // Generate the dash texture by clobbering any existing texture
        vec![
            render::RenderAction::Create1DTextureMono(DASH_TEXTURE, DASH_WIDTH),
            render::RenderAction::WriteTexture1D(DASH_TEXTURE, 0, DASH_WIDTH, Arc::new(pixels)),
            render::RenderAction::CreateMipMaps(DASH_TEXTURE)
        ]
    }

    ///
    /// Returns the render actions needed to update from the specified state to this state (in reverse order, for replaying as a render stack)
    ///
    fn update_from_state(&self, from: &RenderStreamState) -> Vec<render::RenderAction> {
        let mut updates = vec![];
        let mut reset_render_target = false;

        // If the clip buffers are different, make sure we reset the render target state (note that updates are run in reverse order!)
        if let Some(clip_buffers) = &self.clip_buffers {
            if Some(clip_buffers) != from.clip_buffers.as_ref() && clip_buffers.len() > 0 {
                reset_render_target = true;
            }
        }

        // Update the transform state
        if let Some(transform) = self.transform {
            if Some(transform) != from.transform || (self.render_target != from.render_target && self.render_target.is_some()) || reset_render_target {
                updates.push(render::RenderAction::SetTransform(transform_to_matrix(&transform)));
            }
        }

        // Update the shader we're using
        if let (Some(erase), Some(clip), Some(modifier)) = (self.erase_mask.value(), self.clip_mask.value(), &self.shader_modifier) {
            let mask_textures_changed   = Some(erase) != from.erase_mask.value() || Some(clip) != from.clip_mask.value();
            let render_target_changed   = self.render_target != from.render_target && self.render_target.is_some();
            let modifier_changed        = Some(modifier) != from.shader_modifier.as_ref();

            if mask_textures_changed || render_target_changed || reset_render_target || modifier_changed {
                // Pick the shader based on the modifier
                let shader = match modifier {
                    ShaderModifier::Simple                                      => render::ShaderType::Simple { erase_texture: erase, clip_texture: clip },
                    ShaderModifier::DashPattern(_)                              => render::ShaderType::DashedLine { dash_texture: DASH_TEXTURE, erase_texture: erase, clip_texture: clip },
                    ShaderModifier::Texture(texture_id, matrix, repeat, alpha)  => render::ShaderType::Texture { texture: *texture_id, texture_transform: *matrix, repeat: *repeat, alpha: *alpha, erase_texture: erase, clip_texture: clip },
                    ShaderModifier::Gradient(texture_id, matrix, repeat, alpha) => render::ShaderType::LinearGradient { texture: *texture_id, texture_transform: *matrix, repeat: *repeat, alpha: *alpha, erase_texture: erase, clip_texture: clip }
                };

                // Add to the updates
                updates.push(render::RenderAction::UseShader(shader));
            }

            // Generate the texture for the modifier if that's changed
            if modifier_changed {
                match modifier {
                    ShaderModifier::Simple                          => { }
                    ShaderModifier::DashPattern(new_dash_pattern)   => { updates.extend(self.generate_dash_pattern(new_dash_pattern).into_iter().rev()); }
                    ShaderModifier::Texture(_, _, _, _)             => { }
                    ShaderModifier::Gradient(_, _, _, _)            => { }
                }
            }
        }

        // Set the blend mode
        if let Some(blend_mode) = self.blend_mode {
            if Some(blend_mode) != from.blend_mode || (self.render_target != from.render_target && self.render_target.is_some()) || reset_render_target {
                updates.push(render::RenderAction::BlendMode(blend_mode));
            }
        }

        // Choose the render target
        if let Some(render_target) = self.render_target {
            if Some(render_target) != from.render_target || reset_render_target {
                updates.push(render::RenderAction::SelectRenderTarget(render_target));
            }
        }

        // Update the content of the clip mask render target
        if let (Some(clip_buffers), Some(transform)) = (&self.clip_buffers, self.transform) {
            if Some(clip_buffers) != from.clip_buffers.as_ref() && clip_buffers.len() > 0 {
                let render_clip_buffers = clip_buffers.iter()
                    .rev()
                    .map(|(vertices, indices, length)| render::RenderAction::DrawIndexedTriangles(*vertices, *indices, *length));

                // Render the clip buffers once the state is set up (note: actions running in reverse!)
                updates.extend(render_clip_buffers);

                // Set up to render the clip buffers
                updates.extend(vec![
                    render::RenderAction::SetTransform(transform_to_matrix(&transform)),
                    render::RenderAction::BlendMode(render::BlendMode::AllChannelAlphaSourceOver),
                    render::RenderAction::Clear(render::Rgba8([0,0,0,255])),
                    render::RenderAction::UseShader(render::ShaderType::Simple { clip_texture: None, erase_texture: None }),
                    render::RenderAction::SelectRenderTarget(CLIP_RENDER_TARGET)
                ]);
            }
        }

        updates
    }
}

impl RenderCore {
    ///
    /// Generates the rendering actions for the layer with the specified handle
    ///
    /// The render state passed in is the expected state after this rendering has completed, and is updated to be the expected state
    /// before the rendering is completed. This slightly weird arrangement is because the rendering operations are returned as a stack:
    /// ie, they'll run in reverse order.
    ///
    fn render_layer(&mut self, viewport_transform: canvas::Transform2D, layer_handle: LayerHandle, render_state: &mut RenderStreamState) -> Vec<render::RenderAction> {
        use self::RenderEntity::*;

        let core = self;

        // Render the layer in reverse order (this is a stack, so operations are run in reverse order)
        let mut render_layer_stack      = vec![];
        let mut active_transform        = canvas::Transform2D::identity();
        let mut use_erase_texture       = false;
        let mut layer                   = core.layer(layer_handle);

        render_state.transform          = Some(viewport_transform);
        render_state.blend_mode         = Some(render::BlendMode::DestinationOver);
        render_state.render_target      = Some(MAIN_RENDER_TARGET);
        render_state.erase_mask         = Maybe::None;
        render_state.clip_mask          = Maybe::None;
        render_state.clip_buffers       = Some(vec![]);
        render_state.shader_modifier    = Some(ShaderModifier::Simple);

        for render_idx in 0..layer.render_order.len() {
            match &layer.render_order[render_idx] {
                Missing => {
                    // Temporary state while sending a vertex buffer?
                    panic!("Tessellation is not complete (vertex buffer went missing)");
                },

                Tessellating(_id) => { 
                    // Being processed? (shouldn't happen)
                    panic!("Tessellation is not complete (tried to render too early)");
                },

                VertexBuffer(_buffers, _) => {
                    // Should already have sent all the vertex buffers
                    panic!("Tessellation is not complete (found unexpected vertex buffer in layer)");
                },

                RenderSprite(sprite_id, sprite_transform) => { 
                    let sprite_id           = *sprite_id;
                    let sprite_transform    = *sprite_transform;

                    if let Some(sprite_layer) = core.sprites.get(&sprite_id) {
                        let sprite_layer = *sprite_layer;

                        // The sprite transform is appended to the viewport transform
                        let combined_transform  = &viewport_transform * &active_transform;
                        let sprite_transform    = combined_transform * sprite_transform;

                        // The items from before the sprite should be rendered using the current state
                        let old_state           = render_state.clone();

                        // Render the layer associated with the sprite
                        let render_sprite       = core.render_layer(sprite_transform, sprite_layer, render_state);

                        // Items before the sprite are rendered using the 'pre-sprite' rendering
                        render_layer_stack.extend(old_state.update_from_state(render_state));

                        // ... before that, the sprite is renderered
                        render_layer_stack.extend(render_sprite);

                        // ... using its render state
                        render_layer_stack.extend(render_state.update_from_state(&old_state));

                        // Following instructions are rendered using the state before the sprite
                        *render_state           = old_state;
                    }

                    // Reborrow the layer
                    layer                   = core.layer(layer_handle);
                },

                SetTransform(new_transform) => {
                    // The new transform will apply to all the following render instructions
                    active_transform        = *new_transform;

                    // The preceding instructions should render according to the previous state
                    let old_state           = render_state.clone();
                    render_state.transform  = Some(&viewport_transform * &active_transform);

                    render_layer_stack.extend(old_state.update_from_state(render_state));
                },

                SetBlendMode(new_blend_mode) => {
                    let mut old_state   = render_state.clone();

                    if new_blend_mode == &render::BlendMode::DestinationOut {
                        // The previous state should use the eraser texture that we're abount to generate
                        if old_state.render_target == Some(MAIN_RENDER_TARGET) {
                            old_state.erase_mask = Maybe::Some(ERASE_RENDER_TEXTURE);
                        }

                        // Render to the eraser texture
                        render_state.blend_mode     = Some(render::BlendMode::AllChannelAlphaDestinationOver);
                        render_state.render_target  = Some(ERASE_RENDER_TARGET);
                        render_state.erase_mask     = Maybe::None;

                        // Flag that we're using the erase texture and it needs to be cleared for this layer
                        use_erase_texture       = true;
                    } else {
                        // Render the main buffer
                        render_state.blend_mode     = Some(*new_blend_mode);
                        render_state.render_target  = Some(MAIN_RENDER_TARGET);

                        // Use the eraser texture if one is specified
                        if use_erase_texture {
                            render_state.erase_mask = Maybe::Some(ERASE_RENDER_TEXTURE);
                        } else {
                            render_state.erase_mask = Maybe::None;
                        }
                    }

                    // Apply the old state for the preceding instructions
                    render_layer_stack.extend(old_state.update_from_state(render_state));
                },

                DrawIndexed(vertex_buffer, index_buffer, num_items) => {
                    // Draw the triangles
                    render_layer_stack.push(render::RenderAction::DrawIndexedTriangles(*vertex_buffer, *index_buffer, *num_items));
                },

                EnableClipping(vertex_buffer, index_buffer, buffer_size) => {
                    // The preceding instructions should render according to the previous state
                    let old_state               = render_state.clone();
                    render_state.clip_mask      = Maybe::Some(CLIP_RENDER_TEXTURE);
                    render_state.clip_buffers.get_or_insert_with(|| vec![]).push((*vertex_buffer, *index_buffer, *buffer_size));

                    // Apply the old state for the preceding instructions
                    render_layer_stack.extend(old_state.update_from_state(render_state));
                }

                DisableClipping => {
                    // Remove the clip mask from the state
                    let old_state               = render_state.clone();
                    render_state.clip_mask      = Maybe::None;
                    render_state.clip_buffers   = Some(vec![]);

                    // Apply the old state for the preceding instructions
                    render_layer_stack.extend(old_state.update_from_state(render_state));
                }

                SetFlatColor => {
                    // Set the shader modifier to use the dash pattern (overriding any other shader modifier)
                    let old_state                   = render_state.clone();
                    render_state.shader_modifier    = Some(ShaderModifier::Simple);

                    // Apply the old state for the preceding instructions
                    render_layer_stack.extend(old_state.update_from_state(render_state));
                }

                SetDashPattern(dash_pattern) => {
                    // Set the shader modifier to use the dash pattern (overriding any other shader modifier)
                    let old_state               = render_state.clone();
                    if dash_pattern.len() > 0 {
                        render_state.shader_modifier = Some(ShaderModifier::DashPattern(dash_pattern.clone()));
                    } else {
                        render_state.shader_modifier = Some(ShaderModifier::Simple);
                    }

                    // Apply the old state for the preceding instructions
                    render_layer_stack.extend(old_state.update_from_state(render_state));
                }

                SetFillTexture(texture_id, matrix, repeat, alpha) => {
                    // Set the shader modifier to use the fill texture (overriding any other shader modifier)
                    let old_state               = render_state.clone();
                    render_state.shader_modifier = Some(ShaderModifier::Texture(*texture_id, *matrix, *repeat, *alpha));

                    // Apply the old state for the preceding instructions
                    render_layer_stack.extend(old_state.update_from_state(render_state));
                }

                SetFillGradient(texture_id, matrix, repeat, alpha) => {
                    // Set the shader modifier to use the gradient texture (overriding any other shader modifier)
                    let old_state                   = render_state.clone();
                    render_state.shader_modifier    = Some(ShaderModifier::Gradient(*texture_id, *matrix, *repeat, *alpha));

                    // Apply the old state for the preceding instructions
                    render_layer_stack.extend(old_state.update_from_state(render_state));
                }
            }
        }

        // Clear the erase mask if it's used on this layer
        if use_erase_texture {
            render_state.render_target.map(|render_target| {
                render_layer_stack.push(render::RenderAction::SelectRenderTarget(render_target));
            });

            render_layer_stack.push(render::RenderAction::Clear(render::Rgba8([0, 0, 0, 0])));
            render_layer_stack.push(render::RenderAction::SelectRenderTarget(ERASE_RENDER_TARGET));
        }

        // Generate a pending set of actions for the current layer
        return render_layer_stack;
    }
}

impl<'a> RenderStream<'a> {
    ///
    /// Adds the instructions required to render the background colour to the pending queue
    ///
    fn render_background(&mut self) {
        let background_color = self.core.sync(|core| core.background_color);

        // If there's a background colour, then the finalize step should draw it (the OpenGL renderer has issues blitting alpha blended multisampled textures, so this hides that the 'clear' step above doesn't work there)
        let render::Rgba8([br, bg, bb, ba]) = background_color;

        if ba > 0 {
            let background_color = [br, bg, bb, ba];

            self.pending_stack.extend(vec![
                render::RenderAction::DrawTriangles(self.background_vertex_buffer, 0..6),
                render::RenderAction::UseShader(render::ShaderType::Simple { erase_texture: None, clip_texture: None }),
                render::RenderAction::BlendMode(render::BlendMode::DestinationOver),
                render::RenderAction::SetTransform(render::Matrix::identity()),

                // Generate a full-screen quad
                render::RenderAction::CreateVertex2DBuffer(self.background_vertex_buffer, vec![
                    render::Vertex2D { pos: [-1.0, -1.0],   tex_coord: [0.0, 0.0], color: background_color },
                    render::Vertex2D { pos: [1.0, 1.0],     tex_coord: [0.0, 0.0], color: background_color },
                    render::Vertex2D { pos: [1.0, -1.0],    tex_coord: [0.0, 0.0], color: background_color },

                    render::Vertex2D { pos: [-1.0, -1.0],   tex_coord: [0.0, 0.0], color: background_color },
                    render::Vertex2D { pos: [1.0, 1.0],     tex_coord: [0.0, 0.0], color: background_color },
                    render::Vertex2D { pos: [-1.0, 1.0],    tex_coord: [0.0, 0.0], color: background_color },
                ])
            ]);
        }
    }
}

impl<'a> Stream for RenderStream<'a> {
    type Item = render::RenderAction;

    fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<render::RenderAction>> { 
        // Return the next pending action if there is one
        if self.pending_stack.len() > 0 {
            // Note that pending is a stack, so the items are returned in reverse
            return Poll::Ready(self.pending_stack.pop());
        }

        // Poll the tessellation process if it's still running
        if let Some(processing_future) = self.processing_future.as_mut() {
            // Poll the future and send over any vertex buffers that might be waiting
            if processing_future.poll_unpin(context) == Poll::Pending {
                // Still generating render buffers
                // TODO: can potentially send the buffers to the renderer when they're generated here
                return Poll::Pending;
            } else {
                // Finished processing the rendering: can send the actual rendering commands to the hardware layer
                // Layers are rendered in reverse order
                self.processing_future  = None;
                self.layer_id           = self.core.sync(|core| core.layers.len());
                self.render_index       = 0;

                // Perform any setup actions that might exist or have been generated before proceeding
                let (setup_actions, release_textures)   = self.core.sync(|core| (mem::take(&mut core.setup_actions), core.free_unused_textures()));
                
                // TODO: would be more memory efficient to release the textures first, but it's possible for the texture setup to create and never use a texture that is then released...
                self.pending_stack.extend(release_textures);
                self.pending_stack.extend(setup_actions.into_iter().rev());

                if let Some(next) = self.pending_stack.pop() {
                    return Poll::Ready(Some(next));
                }
            }
        }

        // We've generated all the vertex buffers: if frame rendering is suspended, stop here
        if self.frame_suspended {
            if let Some(final_actions) = self.final_stack.take() {
                self.pending_stack = final_actions;
                return Poll::Ready(self.pending_stack.pop());
            } else {
                return Poll::Ready(None);
            }
        }

        // We've generated all the vertex buffers: generate the instructions to render them
        let mut layer_id        = self.layer_id;
        let viewport_transform  = self.viewport_transform;

        let result              = if layer_id == 0 {
            // Stop if we've processed all the layers
            None
        } else {
            // Move to the previous layer
            layer_id -= 1;

            self.core.sync(|core| {
                // Send any pending vertex buffers, then render the layer (note that the rendering is a stack, so the vertex buffers go on the end)
                let layer_handle        = core.layers[layer_id];
                let send_vertex_buffers = core.send_vertex_buffers(layer_handle);
                let mut render_state    = RenderStreamState::new();

                let mut render_layer    = core.render_layer(viewport_transform, layer_handle, &mut render_state);
                render_layer.extend(render_state.update_from_state(&RenderStreamState::new()));
                render_layer.extend(send_vertex_buffers);

                Some(render_layer)
            })
        };

        // Update the layer ID to continue iterating
        self.layer_id       = layer_id;

        // Add the result to the pending queue
        if let Some(result) = result {
            // There are more actions to add to the pending stack
            self.pending_stack = result;
            return Poll::Ready(self.pending_stack.pop());
        } else if let Some(final_actions) = self.final_stack.take() {
            // There are no more drawing actions, but we have a set of final post-render instructions to execute
            self.pending_stack = final_actions;
            self.render_background();
            return Poll::Ready(self.pending_stack.pop());
        } else {
            // No further actions if the result was empty
            return Poll::Ready(None);
        }
    }
}