@@ -10,10 +10,18 @@ pub use a3s_graphics::{
1010 Primitive , Rect , Scene , SceneBuilder , Size , StrokeRect , SCENE_SCHEMA_VERSION ,
1111} ;
1212
13- #[ cfg( feature = "software-reference" ) ]
14- use a3s_graphics:: { FramePlanner , SoftwareRenderer } ;
13+ #[ cfg( feature = "gpu" ) ]
14+ pub use a3s_graphics:: {
15+ GpuBackend , GpuBackendPreference , GpuCapabilities , GpuDeviceType , GpuFrame , GpuPowerPreference ,
16+ GpuReadback , GpuRendererOptions ,
17+ } ;
1518
19+ #[ cfg( any( feature = "software-reference" , feature = "gpu" ) ) ]
20+ use a3s_graphics:: FramePlanner ;
1621#[ cfg( feature = "software-reference" ) ]
22+ use a3s_graphics:: SoftwareRenderer ;
23+
24+ #[ cfg( any( feature = "software-reference" , feature = "gpu" ) ) ]
1725use crate :: GuiResult ;
1826
1927/// Metadata and pixels produced by one deterministic reference frame.
@@ -95,6 +103,69 @@ impl ReferenceRenderer {
95103 }
96104}
97105
106+ /// GPU renderer that plans retained frames directly from GUI-produced scenes.
107+ #[ cfg( feature = "gpu" ) ]
108+ #[ derive( Debug ) ]
109+ pub struct GpuSceneRenderer {
110+ planner : FramePlanner ,
111+ renderer : a3s_graphics:: GpuRenderer ,
112+ }
113+
114+ #[ cfg( feature = "gpu" ) ]
115+ impl GpuSceneRenderer {
116+ pub async fn request ( options : GpuRendererOptions ) -> GuiResult < Self > {
117+ Ok ( Self {
118+ planner : FramePlanner :: new ( ) ,
119+ renderer : a3s_graphics:: GpuRenderer :: request ( options) . await ?,
120+ } )
121+ }
122+
123+ pub fn capabilities ( & self ) -> & GpuCapabilities {
124+ self . renderer . capabilities ( )
125+ }
126+
127+ pub async fn render ( & mut self , scene : Scene ) -> GuiResult < GpuFrame > {
128+ let frame = self . planner . plan ( scene) ?;
129+ Ok ( self . renderer . render ( & frame) . await ?)
130+ }
131+
132+ pub async fn request_readback ( & self ) -> GuiResult < GpuReadbackTicket > {
133+ Ok ( GpuReadbackTicket {
134+ inner : self . renderer . request_readback ( ) . await ?,
135+ } )
136+ }
137+
138+ pub fn poll ( & self ) -> GuiResult < bool > {
139+ Ok ( self . renderer . poll ( ) ?)
140+ }
141+
142+ pub fn wait ( & self ) -> GuiResult < ( ) > {
143+ Ok ( self . renderer . wait ( ) ?)
144+ }
145+
146+ pub fn reset ( & mut self ) {
147+ self . planner . reset ( ) ;
148+ }
149+ }
150+
151+ /// GUI-error-aware handle for an asynchronous GPU readback.
152+ #[ cfg( feature = "gpu" ) ]
153+ #[ derive( Debug ) ]
154+ pub struct GpuReadbackTicket {
155+ inner : a3s_graphics:: GpuReadbackTicket ,
156+ }
157+
158+ #[ cfg( feature = "gpu" ) ]
159+ impl GpuReadbackTicket {
160+ pub fn try_finish ( & mut self ) -> GuiResult < Option < GpuReadback > > {
161+ Ok ( self . inner . try_finish ( ) ?)
162+ }
163+
164+ pub fn finish ( self ) -> GuiResult < GpuReadback > {
165+ Ok ( self . inner . finish ( ) ?)
166+ }
167+ }
168+
98169#[ cfg( all( test, feature = "software-reference" ) ) ]
99170mod tests {
100171 use super :: * ;
@@ -157,3 +228,64 @@ mod tests {
157228 assert_send_sync :: < ReferenceRenderer > ( ) ;
158229 }
159230}
231+
232+ #[ cfg( all( test, feature = "gpu" ) ) ]
233+ mod gpu_tests {
234+ use super :: * ;
235+ use crate :: GuiError ;
236+
237+ fn scene ( ) -> Scene {
238+ let mut builder = SceneBuilder :: new ( Size :: new ( 4.0 , 3.0 ) , 1.0 , Color :: WHITE ) ;
239+ builder
240+ . push ( DrawCommand :: new (
241+ DrawId :: new ( 1 ) . unwrap ( ) ,
242+ Primitive :: FillRect ( FillRect {
243+ rect : Rect :: new ( 1.0 , 1.0 , 2.0 , 1.0 ) ,
244+ color : Color :: BLACK ,
245+ } ) ,
246+ ) )
247+ . unwrap ( ) ;
248+ builder. finish ( ) . unwrap ( )
249+ }
250+
251+ #[ test]
252+ fn invalid_gpu_options_keep_the_gui_error_boundary ( ) {
253+ let options = GpuRendererOptions {
254+ max_instances_per_frame : 0 ,
255+ ..Default :: default ( )
256+ } ;
257+ let error = pollster:: block_on ( GpuSceneRenderer :: request ( options) ) . unwrap_err ( ) ;
258+ assert ! ( matches!( error, GuiError :: Graphics { .. } ) ) ;
259+ assert ! ( error. to_string( ) . contains( "max_instances_per_frame" ) ) ;
260+ }
261+
262+ #[ test]
263+ fn gpu_scene_renderer_completes_the_gui_scene_boundary ( ) {
264+ let options = GpuRendererOptions {
265+ power_preference : GpuPowerPreference :: None ,
266+ allow_software_adapter : true ,
267+ ..Default :: default ( )
268+ } ;
269+ let mut renderer = match pollster:: block_on ( GpuSceneRenderer :: request ( options) ) {
270+ Ok ( renderer) => renderer,
271+ Err ( error) if error. to_string ( ) . contains ( "no compatible GPU adapter" ) => return ,
272+ Err ( error) => panic ! ( "failed to initialize GUI test GPU: {error}" ) ,
273+ } ;
274+
275+ let frame = pollster:: block_on ( renderer. render ( scene ( ) ) ) . unwrap ( ) ;
276+ assert ! ( frame. rendered) ;
277+ let image = pollster:: block_on ( renderer. request_readback ( ) )
278+ . unwrap ( )
279+ . finish ( )
280+ . unwrap ( ) ;
281+ assert_eq ! ( image. pixel( 1 , 1 ) , Some ( Color :: BLACK ) ) ;
282+ }
283+
284+ #[ test]
285+ fn gpu_scene_renderer_is_send_and_sync ( ) {
286+ fn assert_send_sync < T : Send + Sync > ( ) { }
287+ fn assert_send < T : Send > ( ) { }
288+ assert_send_sync :: < GpuSceneRenderer > ( ) ;
289+ assert_send :: < GpuReadbackTicket > ( ) ;
290+ }
291+ }
0 commit comments