@@ -26,6 +26,12 @@ const MAX_TRANSCRIPT_ENTRIES := 50
2626## Refresh delay in seconds (wait for user to stop navigating)
2727const REFRESH_DELAY := 0.15
2828
29+ @export_group ("Performance" )
30+ ## If true, bake a CPU ImageTexture with mipmaps from the viewport (expensive).
31+ @export var bake_viewport_mipmaps : bool = false
32+ ## Delay before rebuilding transcript pages after new entries (seconds).
33+ @export var transcript_update_delay_sec : float = 0.25
34+
2935## Sound to play when page is changed.
3036@export var page_change_sounds : Array [AudioStream ] = [
3137 preload ("res://audio/sfx/sfx_paper_flip_01.wav" ),
@@ -69,7 +75,7 @@ var _transcript_entries: Array[Dictionary] = []
6975
7076## Transcript update mutex to prevent concurrent updates
7177var _transcript_updating := false
72- var _transcript_pending_entries : Array [ Dictionary ] = []
78+ var _transcript_update_needs_rerun := false
7379
7480## Current scenario reference
7581var _scenario : ScenarioData
@@ -85,6 +91,7 @@ var _briefing_material: StandardMaterial3D
8591## Debounce timers for texture refresh
8692var _intel_refresh_timer : Timer
8793var _transcript_refresh_timer : Timer
94+ var _transcript_update_timer : Timer
8895var _briefing_refresh_timer : Timer
8996
9097
@@ -126,24 +133,30 @@ func _setup_viewports() -> void:
126133 _intel_viewport = SubViewport .new ()
127134 _intel_viewport .size = render_size
128135 _intel_viewport .transparent_bg = false
129- _intel_viewport .render_target_update_mode = SubViewport .UPDATE_ALWAYS
136+ _intel_viewport .render_target_update_mode = SubViewport .UPDATE_DISABLED
130137 _intel_viewport .gui_disable_input = false
138+ _intel_viewport .msaa_2d = Viewport .MSAA_2X
139+ _intel_viewport .screen_space_aa = Viewport .SCREEN_SPACE_AA_DISABLED
131140 add_child (_intel_viewport )
132141
133142 # Transcript viewport
134143 _transcript_viewport = SubViewport .new ()
135144 _transcript_viewport .size = render_size
136145 _transcript_viewport .transparent_bg = false
137- _transcript_viewport .render_target_update_mode = SubViewport .UPDATE_ALWAYS
146+ _transcript_viewport .render_target_update_mode = SubViewport .UPDATE_DISABLED
138147 _transcript_viewport .gui_disable_input = false
148+ _transcript_viewport .msaa_2d = Viewport .MSAA_2X
149+ _transcript_viewport .screen_space_aa = Viewport .SCREEN_SPACE_AA_DISABLED
139150 add_child (_transcript_viewport )
140151
141152 # Briefing viewport
142153 _briefing_viewport = SubViewport .new ()
143154 _briefing_viewport .size = render_size
144155 _briefing_viewport .transparent_bg = false
145- _briefing_viewport .render_target_update_mode = SubViewport .UPDATE_ALWAYS
156+ _briefing_viewport .render_target_update_mode = SubViewport .UPDATE_DISABLED
146157 _briefing_viewport .gui_disable_input = false
158+ _briefing_viewport .msaa_2d = Viewport .MSAA_2X
159+ _briefing_viewport .screen_space_aa = Viewport .SCREEN_SPACE_AA_DISABLED
147160 add_child (_briefing_viewport )
148161
149162
@@ -163,6 +176,13 @@ func _setup_refresh_timers() -> void:
163176 _transcript_refresh_timer .timeout .connect (_do_transcript_refresh )
164177 add_child (_transcript_refresh_timer )
165178
179+ # Transcript update timer (coalesce new entries)
180+ _transcript_update_timer = Timer .new ()
181+ _transcript_update_timer .wait_time = maxf (transcript_update_delay_sec , 0.01 )
182+ _transcript_update_timer .one_shot = true
183+ _transcript_update_timer .timeout .connect (_do_transcript_update )
184+ add_child (_transcript_update_timer )
185+
166186 # Briefing timer
167187 _briefing_refresh_timer = Timer .new ()
168188 _briefing_refresh_timer .wait_time = REFRESH_DELAY
@@ -375,24 +395,25 @@ func _update_transcript_content(follow_new_messages: bool) -> void:
375395 _transcript_face .update_page_indicator ()
376396 _display_page (_transcript_face , _transcript_content , _transcript_pages , target_page )
377397
378- # Refresh texture immediately for transcript updates (no debounce needed)
379- await _do_transcript_refresh ()
398+ if _transcript_refresh_timer :
399+ _transcript_refresh_timer . start ()
380400
381401
382- ## Add a radio transmission to the transcript
383- ## [param speaker] Who is speaking (e.g., "PLAYER", "ALPHA", "HQ")
384- ## [param message] The message text
385- func add_transcript_entry (speaker : String , message : String ) -> void :
386- var timestamp := _get_mission_timestamp ()
387- var entry := {"timestamp" : timestamp , "speaker" : speaker , "message" : message }
388-
389- _transcript_entries .append (entry )
402+ func _queue_transcript_update () -> void :
403+ if _transcript_update_timer == null :
404+ return
405+ if _transcript_updating :
406+ _transcript_update_needs_rerun = true
407+ return
408+ _transcript_update_timer .wait_time = maxf (transcript_update_delay_sec , 0.01 )
409+ if not _transcript_update_timer .is_stopped ():
410+ return
411+ _transcript_update_timer .start ()
390412
391- if _transcript_entries .size () > MAX_TRANSCRIPT_ENTRIES :
392- _transcript_entries .pop_front ()
393413
414+ func _do_transcript_update () -> void :
394415 if _transcript_updating :
395- _transcript_pending_entries . append ( entry )
416+ _transcript_update_needs_rerun = true
396417 return
397418
398419 _transcript_updating = true
@@ -405,22 +426,24 @@ func add_transcript_entry(speaker: String, message: String) -> void:
405426
406427 _transcript_updating = false
407428
408- if _transcript_pending_entries . size () > 0 :
409- _transcript_pending_entries . clear ()
410- await _refresh_transcript_display ()
429+ if _transcript_update_needs_rerun :
430+ _transcript_update_needs_rerun = false
431+ _queue_transcript_update ()
411432
412433
413- ## Refresh transcript display without adding new entries
414- func _refresh_transcript_display () -> void :
415- _transcript_updating = true
434+ ## Add a radio transmission to the transcript
435+ ## [param speaker] Who is speaking (e.g., "PLAYER", "ALPHA", "HQ")
436+ ## [param message] The message text
437+ func add_transcript_entry (speaker : String , message : String ) -> void :
438+ var timestamp : String = _get_mission_timestamp ()
439+ var entry : Dictionary = {"timestamp" : timestamp , "speaker" : speaker , "message" : message }
416440
417- var was_on_last_page := false
418- if _transcript_face and _transcript_pages .size () > 0 :
419- was_on_last_page = _transcript_face .current_page >= _transcript_pages .size () - 1
441+ _transcript_entries .append (entry )
420442
421- await _update_transcript_content (was_on_last_page )
443+ if _transcript_entries .size () > MAX_TRANSCRIPT_ENTRIES :
444+ _transcript_entries .pop_front ()
422445
423- _transcript_updating = false
446+ _queue_transcript_update ()
424447
425448
426449## Get current mission timestamp as formatted string
@@ -434,6 +457,14 @@ func _get_mission_timestamp() -> String:
434457
435458## Apply rendered textures to clipboard materials
436459func _apply_textures () -> void :
460+ # Render each SubViewport once before capturing to textures.
461+ if _intel_viewport :
462+ _intel_viewport .render_target_update_mode = SubViewport .UPDATE_ONCE
463+ if _transcript_viewport :
464+ _transcript_viewport .render_target_update_mode = SubViewport .UPDATE_ONCE
465+ if _briefing_viewport :
466+ _briefing_viewport .render_target_update_mode = SubViewport .UPDATE_ONCE
467+
437468 await get_tree ().process_frame
438469 await get_tree ().process_frame # Extra frame to ensure render complete
439470
@@ -445,6 +476,7 @@ func _apply_textures() -> void:
445476## Refresh the transcript document texture after content updates
446477func _refresh_transcript_texture () -> void :
447478 if _transcript_material and _transcript_viewport :
479+ _transcript_viewport .render_target_update_mode = SubViewport .UPDATE_ONCE
448480 await get_tree ().process_frame # Wait for render
449481 _refresh_texture (_transcript_material , _transcript_viewport )
450482
@@ -492,6 +524,22 @@ func _refresh_texture(material: StandardMaterial3D, viewport: SubViewport) -> vo
492524 LogService .warning ("Cannot refresh: material or viewport is null" , "DocumentController.gd" )
493525 return
494526
527+ # Reduce glare so text stays readable under strong lights.
528+ material .albedo_color = Color .WHITE
529+ material .metallic = 0.0
530+ material .roughness = 1.0
531+ material .specular = 0.0
532+
533+ material .texture_filter = (
534+ BaseMaterial3D .TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC
535+ if bake_viewport_mipmaps
536+ else BaseMaterial3D .TEXTURE_FILTER_LINEAR
537+ )
538+
539+ if not bake_viewport_mipmaps :
540+ material .albedo_texture = viewport .get_texture ()
541+ return
542+
495543 var img := viewport .get_texture ().get_image ()
496544 if img == null :
497545 LogService .warning ("Failed to get image from viewport" , "DocumentController.gd" )
@@ -713,18 +761,24 @@ func _on_briefing_page_changed(page_index: int) -> void:
713761
714762## Debounced refresh functions - called after timer expires
715763func _do_intel_refresh () -> void :
764+ if _intel_viewport :
765+ _intel_viewport .render_target_update_mode = SubViewport .UPDATE_ONCE
716766 await get_tree ().process_frame
717767 await get_tree ().process_frame
718768 _refresh_texture (_intel_material , _intel_viewport )
719769
720770
721771func _do_transcript_refresh () -> void :
772+ if _transcript_viewport :
773+ _transcript_viewport .render_target_update_mode = SubViewport .UPDATE_ONCE
722774 await get_tree ().process_frame
723775 await get_tree ().process_frame
724776 _refresh_texture (_transcript_material , _transcript_viewport )
725777
726778
727779func _do_briefing_refresh () -> void :
780+ if _briefing_viewport :
781+ _briefing_viewport .render_target_update_mode = SubViewport .UPDATE_ONCE
728782 await get_tree ().process_frame
729783 await get_tree ().process_frame
730784 _refresh_texture (_briefing_material , _briefing_viewport )
0 commit comments