From 7f344ae5c806c63ca0c5761e5bff19aac673c72c Mon Sep 17 00:00:00 2001 From: NeilClack Date: Fri, 24 Apr 2026 11:06:50 -0500 Subject: [PATCH] Optimize proclamation loading --- internal/page/scrollerPage.go | 400 +++++++++++++++------------------- 1 file changed, 177 insertions(+), 223 deletions(-) diff --git a/internal/page/scrollerPage.go b/internal/page/scrollerPage.go index 5476bb6..4612a40 100644 --- a/internal/page/scrollerPage.go +++ b/internal/page/scrollerPage.go @@ -1,59 +1,3 @@ -// ATTENTION: TED -// -// # Integrated Multi-Domain Rhema Wave Generation System -// -// This module implements the complete Rhema Wave generation system by synchronizing multiple -// subsystems that operate simultaneously across spatial, temporal, and chromatic -// domains to produce a unified frequency-structured visual output for subliminal proclamation display. -// -// The technology addresses the fundamental challenge of presenting textual proclamation content -// in a format optimized for subconscious processing rather than conscious reading. Conventional -// subliminal text systems use isolated techniques (simple masking, single-frequency flashing, or -// uniform scrolling) that operate in only one domain. Our system integrates four mathematically- -// synchronized subsystems, each generating Fibonacci-based patterns in different domains, creating -// a compound frequency signature designed to enhance subconscious absorption while maintaining -// conscious illegibility. -// -// The system architecture integrates: -// -// 1. Spatial Character Transformation - Each rendered character undergoes Fibonacci-based -// block scrambling (described separately), creating local spatial frequency patterns at -// the character level. -// -// 2. Spatial Line Distribution - Text lines contain varying word counts following an oscillating -// Fibonacci sequence (1, 1, 2, 3, 5, 8, 13... then reversing), creating a macro-level -// two-dimensional wave pattern in the display geometry and ensuring balanced temporal -// exposure of proclamation content segments. -// -// 3. Temporal Color Modulation - Sequential words receive colors from an intent-based palette, -// with color duration determined by an oscillating Fibonacci sequence, creating rhythmic -// chromatic variation synchronized with natural mathematical progression. -// -// 4. Temporal Velocity Modulation - Display scroll velocity oscillates between negative and -// positive extremes with acceleration/deceleration rates and hold periods, creating a -// "breathing" temporal rhythm for the entire visual field. -// -// The key innovation lies in the temporal and mathematical synchronization of these subsystems. -// All four operate on Fibonacci-derived parameters, creating harmonic relationships between the -// spatial frequencies (character scrambling, line wave patterns), chromatic frequencies (color -// duration oscillation), and temporal frequencies (scroll velocity modulation). This multi-domain -// integration produces what we term the "Rhema Wave" - a compound frequency signature where -// spatial, temporal, and chromatic patterns reinforce each other through mathematical harmony. -// -// Key differentiators from existing solutions: -// - Integrates four distinct Fibonacci-based frequency generators in a single system -// - Synchronizes pattern generation across spatial, temporal, and chromatic domains -// - Creates compound frequency signatures rather than isolated single-domain effects -// - Maintains character-specific and content-specific mathematical structure throughout -// - Uses bidirectional oscillation across multiple subsystems for rhythmic coherence -// - Combines macro-level (line patterns, velocity) and micro-level (character, color) modulation -// -// The system loads proclamation text content, processes it through all four subsystems simultaneously, -// and generates a continuously scrolling display where every element - from individual character -// pixels to overall scroll rhythm - contributes to the unified Rhema Wave output. The mathematical -// relationships between subsystems create interference patterns and harmonic resonances designed -// to enhance subconscious processing of the proclamation content while maintaining conscious -// illegibility through the character scrambling subsystem. package page import ( @@ -77,7 +21,7 @@ import ( const ( defaultProclamationID string = "f3d42354-3197-45e4-9b08-be447f14386d" proclamationPlayCount int = 10 - defaultProclamationPlayCount int = 10 + defaultProclamationPlayCount int = 1 ) type Line struct { @@ -85,6 +29,15 @@ type Line struct { width int32 } +// proclamationStream tracks one proclamation and how many full loops it should play. +type proclamationStream struct { + name string + words []string + repeat int + completed int + wordIndex int +} + // LineDeque is a double-ended queue for Line values backed by a circular buffer. // It avoids O(n) prepend and prevents capacity leaks from head-of-slice removal. type LineDeque struct { @@ -171,8 +124,9 @@ func (d *LineDeque) Clear() { type ScrollerPage struct { Base - words []string - wordIndex int + streams []proclamationStream + streamIndex int + lines *LineDeque lineHeight int32 shader *shaderManager.Shader @@ -185,6 +139,9 @@ type ScrollerPage struct { scrollY float32 fontScale float32 + // Reuse one GL texture per unique word. + wordTextureCache map[string]StringItem + increment float32 velocity float32 @@ -227,13 +184,14 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error { logger.Info("Shader Program ID: %d", textShader.ProgramID) logger.Info("p.shader Program ID: %d", p.shader.ProgramID) - // Load proclamation words from database + p.wordTextureCache = make(map[string]StringItem, 256) + + // Load proclamation streams from database if err := p.loadProclamations(); err != nil { logger.Error("Error loading proclamations: %v", err) p.Context.NavigateTo(PROCLAMATIONS) return nil } - p.wordIndex = 0 // Get line pattern from database var err error @@ -256,25 +214,19 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error { } p.colorPattern = colorPattern.NewColorPattern(baseColors) - // Get font sizes from database (optimize: load once, cache as int32) - // Match the working page's logic exactly: - p.fontScale = float32(32) / float32(p.Context.CfgPtr.StandardFontSize) - // Font setup - nLines := int(float32(p.ScreenHeight) / 32.0) // 32 is your target visual size + p.fontScale = float32(32) / float32(p.Context.CfgPtr.StandardFontSize) + nLines := int(float32(p.ScreenHeight) / 32.0) p.lines = NewLineDeque(nLines * 2) - // Calculate space width using the SCALE ratio, not the font size int + // Calculate space width using the scale ratio. spaceGlyph, ok := fm.GetGlyph(font.Name, ' ') if !ok { return fmt.Errorf("failed to get space glyph") } p.spaceWidth = int32(float32(spaceGlyph.Advance) * p.fontScale) - // Get line height using the SCALE ratio - if fm == nil { - logger.Error("FontManager somehow ended up nil...") - } + // Get line height using the same scale ratio. tmpStrID, _, H, err := fm.NewCreateStringTexture(font.Name, "Mg", p.shader) if err != nil { return fmt.Errorf("failed to create sample texture: %w", err) @@ -290,7 +242,7 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error { } // Create initial lines - for i := 0; i < nLines; i++ { + for i := range nLines { if err := p.createLine(i, true); err != nil { return fmt.Errorf("failed to create initial line %d: %w", i, err) } @@ -298,92 +250,154 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error { // Audio setup audioDeviceName := p.Context.CfgPtr.SelectedAudioDevice - - // Switch audio devices if needed if mixer.CurrentDevice() != audioDeviceName { if err := mixer.SwitchDevice(audioDeviceName); err != nil { - } else { + logger.Error("failed to switch audio device: %v", err) } } - // Start audio playback - - // selectedAudio := p.Context.CfgPtr.SelectedAudio - assetsDir := p.Context.CfgPtr.AssetsDir audioFile := filepath.Join(assetsDir, "audio/audio.wav") logger.Info("Attempting to play %s", audioFile) if err := mixer.Play(audioFile, true, p.Context.CfgPtr); err != nil { - logger.Error("Mixer returned an error: %v", err) // ADD THIS + logger.Error("Mixer returned an error: %v", err) } currentDevice := mixer.CurrentDevice() if currentDevice == "" { currentDevice = "default" } + _ = currentDevice return nil } -// loadProclamations loads selected proclamations from the database +// loadProclamations loads selected proclamations as stream units (not one giant word list). func (p *ScrollerPage) loadProclamations() error { - p.words = []string{} + p.streams = p.streams[:0] - // Get all selected proclamations from database - var proclamations []db.Proclamation - err := p.Context.DbPtr.Where("selected = ?", true).Find(&proclamations).Error - if err != nil { + var selected []db.Proclamation + if err := p.Context.DbPtr.Where("selected = ?", true).Find(&selected).Error; err != nil { return err } var defaultProclamation db.Proclamation - err = p.Context.DbPtr.Where("ID = ?", defaultProclamationID).Find(&defaultProclamation).Error - if err != nil { + if err := p.Context.DbPtr.Where("ID = ?", defaultProclamationID).First(&defaultProclamation).Error; err != nil { return err } - - // Build word list from selected, available proclamations defaultWords := strings.Fields(defaultProclamation.Content) - for _, proclamation := range proclamations { - words := strings.Fields(proclamation.Content) - - // Pre-allocate: 10x proclamation words + 1x default words - batch := make([]string, 0, len(words)*10+len(defaultWords)) - for range 10 { - logger.Debug("Adding %s", proclamation.Name) - batch = append(batch, words...) + // Build interleaved stream: + // each selected proclamation repeats N times, then default repeats once. + for _, proclamation := range selected { + // Prevent default proclamation from being treated as a normal selected proclamation. + if proclamation.ID == defaultProclamationID { + continue } - logger.Debug("Adding %s", defaultProclamation.Name) - batch = append(batch, defaultWords...) - p.words = append(p.words, batch...) + words := strings.Fields(proclamation.Content) + if len(words) == 0 { + continue + } + + p.streams = append(p.streams, proclamationStream{ + name: proclamation.Name, + words: words, + repeat: proclamationPlayCount, // 10 + }) + + if len(defaultWords) > 0 { + p.streams = append(p.streams, proclamationStream{ + name: defaultProclamation.Name, + words: defaultWords, + repeat: 1, // play once between selected proclamations + }) + } } - if len(p.words) == 0 { - return fmt.Errorf("Failed to load proclamations, no proclamations selected!") + // Fallback: if no non-default selected proclamations, at least play default. + if len(p.streams) == 0 && len(defaultWords) > 0 { + p.streams = append(p.streams, proclamationStream{ + name: defaultProclamation.Name, + words: defaultWords, + repeat: 1, + }) } + if len(p.streams) == 0 { + return fmt.Errorf("failed to load proclamations: no proclamation words available") + } + + p.streamIndex = 0 return nil } -// createLine is a helper to generate a new line at a specific index -// isInitial determines if this is during initialization (positions from top) -// or dynamic creation (positions relative to existing lines) -func (p *ScrollerPage) createLine(index int, isInitial bool) error { - words := p.activePattern.GetNextLine(p.words, &p.wordIndex, p.lineCtx) +// nextLineWords gets words for the current line and advances proclamation stream when needed. +func (p *ScrollerPage) nextLineWords() []string { + if len(p.streams) == 0 { + return nil + } + + s := &p.streams[p.streamIndex] + if len(s.words) == 0 { + p.streamIndex = (p.streamIndex + 1) % len(p.streams) + return p.nextLineWords() + } + + prevIdx := s.wordIndex + words := p.activePattern.GetNextLine(s.words, &s.wordIndex, p.lineCtx) + + // If word index wraps, one full proclamation pass completed. + if s.wordIndex < prevIdx { + s.completed++ + targetRepeats := s.repeat + if targetRepeats <= 0 { + targetRepeats = 1 + } + logger.Debug("Now playing proclamation: %s", s.name) + if s.completed >= targetRepeats { + logger.Debug("Advancing proclamation stream: %s", s.name) + s.completed = 0 + s.wordIndex = 0 + p.streamIndex = (p.streamIndex + 1) % len(p.streams) + } + } + + return words +} + +// getCachedWord returns a reusable textured StringItem for a word. +func (p *ScrollerPage) getCachedWord(word string) (StringItem, error) { + if cached, ok := p.wordTextureCache[word]; ok { + return cached, nil + } + + base, err := NewStringItem(word, p.font, sdl.Color{R: 255, G: 255, B: 255, A: 255}) + if err != nil { + return StringItem{}, fmt.Errorf("unable to create cached string item for word '%s': %w", word, err) + } + base.Scale(p.fontScale) + + p.wordTextureCache[word] = base + return base, nil +} + +// buildLineFromWords builds a line using cached texture IDs and per-instance color/position. +func (p *ScrollerPage) buildLineFromWords(words []string) (Line, error) { line := Line{words: []StringItem{}} for _, word := range words { if word == "" { continue } - item, err := NewStringItem(word, p.font, p.colorPattern.Color()) + + cached, err := p.getCachedWord(word) if err != nil { - return fmt.Errorf("unable to create string item for word '%s': %w", word, err) + return Line{}, err } - item.Scale(p.fontScale) + item := cached + item.Color = p.colorPattern.Color() if len(line.words) > 0 { line.width += p.spaceWidth @@ -392,18 +406,12 @@ func (p *ScrollerPage) createLine(index int, isInitial bool) error { line.width += item.W } - // Calculate horizontal position (centered) + return line, nil +} + +// positionLine sets X/Y for all words in a line. +func (p *ScrollerPage) positionLine(line *Line, baseY int32) { startX := p.ScreenCenterX - (line.width / 2) - - // Calculate vertical position - var baseY int32 - if isInitial { - baseY = int32(p.scrollY) - int32(p.lineHeight)*int32(index) - } else { - baseY = 0 // Will be set by caller based on context - } - - // Position all words in the line for j := range line.words { if j == 0 { line.words[j].X = startX @@ -412,6 +420,25 @@ func (p *ScrollerPage) createLine(index int, isInitial bool) error { } line.words[j].Y = baseY } +} + +// createLine is a helper to generate a new line at a specific index. +// isInitial determines if this is during initialization (positions from top) +// or dynamic creation (positions relative to existing lines). +func (p *ScrollerPage) createLine(index int, isInitial bool) error { + words := p.nextLineWords() + line, err := p.buildLineFromWords(words) + if err != nil { + return err + } + + var baseY int32 + if isInitial { + baseY = int32(p.scrollY) - int32(p.lineHeight)*int32(index) + } else { + baseY = 0 + } + p.positionLine(&line, baseY) if isInitial { p.lines.PushBack(line) @@ -463,10 +490,7 @@ func (p *ScrollerPage) Update(dt float32) error { if p.velocity > 0 { // Moving up: replace lines that go off top for p.lines.Len() > 0 && len(p.lines.Front().words) > 0 && p.lines.Front().words[0].Y > p.ScreenHeight { - old := p.lines.PopFront() - for _, word := range old.words { - gl.DeleteTextures(1, &word.ID) - } + _ = p.lines.PopFront() // no texture delete; textures are shared via cache if err := p.addLineAtBottom(); err != nil { return err @@ -480,10 +504,7 @@ func (p *ScrollerPage) Update(dt float32) error { break } - old := p.lines.PopBack() - for _, word := range old.words { - gl.DeleteTextures(1, &word.ID) - } + _ = p.lines.PopBack() // no texture delete; textures are shared via cache if err := p.addLineAtTop(); err != nil { return err @@ -494,32 +515,14 @@ func (p *ScrollerPage) Update(dt float32) error { return nil } -// addLineAtBottom creates and adds a new line at the bottom of the display +// addLineAtBottom creates and adds a new line at the bottom of the display. func (p *ScrollerPage) addLineAtBottom() error { - newWords := p.activePattern.GetNextLine(p.words, &p.wordIndex, p.lineCtx) - line := Line{words: []StringItem{}} - - for _, word := range newWords { - if word == "" { - continue - } - item, err := NewStringItem(word, p.font, p.colorPattern.Color()) - if err != nil { - return fmt.Errorf("unable to create string item for word '%s': %w", word, err) - } - - item.Scale(p.fontScale) - - if len(line.words) > 0 { - line.width += p.spaceWidth - } - line.words = append(line.words, item) - line.width += item.W + newWords := p.nextLineWords() + line, err := p.buildLineFromWords(newWords) + if err != nil { + return err } - startX := p.ScreenCenterX - (line.width / 2) - - // Position relative to last existing line var baseY int32 if p.lines.Len() > 0 && len(p.lines.Back().words) > 0 { baseY = p.lines.Back().words[0].Y - int32(p.lineHeight) @@ -527,45 +530,19 @@ func (p *ScrollerPage) addLineAtBottom() error { baseY = -int32(p.lineHeight) } - for j := range line.words { - if j == 0 { - line.words[j].X = startX - } else { - line.words[j].X = line.words[j-1].X + line.words[j-1].W + p.spaceWidth - } - line.words[j].Y = baseY - } - + p.positionLine(&line, baseY) p.lines.PushBack(line) return nil } -// addLineAtTop creates and adds a new line at the top of the display +// addLineAtTop creates and adds a new line at the top of the display. func (p *ScrollerPage) addLineAtTop() error { - newWords := p.activePattern.GetNextLine(p.words, &p.wordIndex, p.lineCtx) - line := Line{words: []StringItem{}} - - for _, word := range newWords { - if word == "" { - continue - } - item, err := NewStringItem(word, p.font, p.colorPattern.Color()) - if err != nil { - return fmt.Errorf("unable to create string item for word '%s': %w", word, err) - } - - item.Scale(p.fontScale) - - if len(line.words) > 0 { - line.width += p.spaceWidth - } - line.words = append(line.words, item) - line.width += item.W + newWords := p.nextLineWords() + line, err := p.buildLineFromWords(newWords) + if err != nil { + return err } - startX := p.ScreenCenterX - (line.width / 2) - - // Position relative to first existing line var baseY int32 if p.lines.Len() > 0 && len(p.lines.Front().words) > 0 { baseY = p.lines.Front().words[0].Y + int32(p.lineHeight) @@ -573,15 +550,7 @@ func (p *ScrollerPage) addLineAtTop() error { baseY = p.ScreenHeight } - for j := range line.words { - if j == 0 { - line.words[j].X = startX - } else { - line.words[j].X = line.words[j-1].X + line.words[j-1].W + p.spaceWidth - } - line.words[j].Y = baseY - } - + p.positionLine(&line, baseY) p.lines.PushFront(line) return nil } @@ -608,40 +577,25 @@ func (p *ScrollerPage) Destroy() error { mixer.Stop(1000, p.Context.CfgPtr) - // GL cleanup — delete all text textures - if p.lines != nil { - for i := 0; i < p.lines.Len(); i++ { - line := p.lines.Get(i) - for j := range line.words { - if id := line.words[j].ID; id != 0 { - gl.DeleteTextures(1, &line.words[j].ID) - line.words[j].ID = 0 - } - } + // Delete each cached word texture exactly once. + for k, item := range p.wordTextureCache { + if item.ID != 0 { + id := item.ID + gl.DeleteTextures(1, &id) } + delete(p.wordTextureCache, k) } gl.Flush() // Drop references for GC safely - if p.lines != nil { - p.lines = nil - } - if p.words != nil { - p.words = nil - } - if p.lineCtx != nil { - p.lineCtx = nil - } - if p.font != nil { - p.font = nil - } - if p.shader != nil { - p.shader = nil - } - if p.colorPattern != nil { - p.colorPattern = nil - } + p.lines = nil + p.streams = nil + p.lineCtx = nil + p.font = nil + p.shader = nil + p.colorPattern = nil + p.wordTextureCache = nil return nil } @@ -662,7 +616,7 @@ func (p *ScrollerPage) SetAutoVelocity(enabled bool, floor, ceiling, step, delay p.velocity = p.velocityFloor } -// updateAutoVelocity handles automatic velocity changes +// updateAutoVelocity handles automatic velocity changes. func (p *ScrollerPage) updateAutoVelocity(dt float32) { if !p.autoVelocity { return