make bootup faster

This commit is contained in:
Yonokid
2025-07-24 16:06:09 -04:00
parent d8acf8a4d6
commit 718d97cba7
7 changed files with 190 additions and 172 deletions

View File

@@ -2,7 +2,6 @@ import sqlite3
from pathlib import Path
import pyray as ray
from dotenv import dotenv_values
from raylib.defines import (
RL_FUNC_ADD,
RL_ONE,
@@ -59,21 +58,11 @@ def create_song_db():
print("Scores database created successfully")
def main():
env_config = dotenv_values(".env")
create_song_db()
song_hash.song_hashes = song_hash.build_song_hashes()
global_data.song_hashes = song_hash.build_song_hashes()
global_data.config = get_config()
screen_width: int = global_data.config["video"]["screen_width"]
screen_height: int = global_data.config["video"]["screen_height"]
'''
render_width, render_height = ray.get_render_width(), ray.get_render_height()
dpi_scale = ray.get_window_scale_dpi()
if dpi_scale.x == 0:
dpi_scale = (ray.get_render_width(), ray.get_render_height())
dpi_scale = screen_width, screen_height
else:
dpi_scale = int(render_width/dpi_scale.x), int(render_height/dpi_scale.y)
'''
if global_data.config["video"]["vsync"]:
ray.set_config_flags(ray.ConfigFlags.FLAG_VSYNC_HINT)
@@ -113,12 +102,7 @@ def main():
ray.rl_set_blend_factors_separate(RL_SRC_ALPHA, RL_ONE_MINUS_SRC_ALPHA, RL_ONE, RL_ONE_MINUS_SRC_ALPHA, RL_FUNC_ADD, RL_FUNC_ADD)
ray.set_exit_key(ray.KeyboardKey.KEY_A)
global_data.textures = load_all_textures_from_zip(Path('Graphics/lumendata/intermission.zip'))
prev_ms = get_current_ms()
while not ray.window_should_close():
current_ms = get_current_ms()
if current_ms >= prev_ms + 100:
print("LAG SPIKE DETECTED")
prev_ms = current_ms
ray.begin_texture_mode(target)
ray.begin_blend_mode(ray.BlendMode.BLEND_CUSTOM_SEPARATE)
@@ -140,7 +124,6 @@ def main():
ray.end_texture_mode()
ray.begin_drawing()
ray.clear_background(ray.WHITE)
#Thanks to rnoiz proper render height
ray.draw_texture_pro(
target.texture,
ray.Rectangle(0, 0, target.texture.width, -target.texture.height),

View File

@@ -1,16 +1,138 @@
import csv
import json
import sys
import time
from collections import deque
from pathlib import Path
from typing import Optional
from git import Repo
from libs.tja import TJAParser
from libs.utils import get_config
from libs.utils import get_config, global_data
song_hashes: Optional[dict] = None
class DiffHashesDecoder(json.JSONDecoder):
def __init__(self, *args, **kwargs):
super().__init__(object_hook=self.object_hook, *args, **kwargs)
def object_hook(self, obj):
if "diff_hashes" in obj:
obj["diff_hashes"] = {
int(key): value
for key, value in obj["diff_hashes"].items()
}
return obj
def build_song_hashes(output_dir=Path("cache")):
song_hashes: dict[str, list[dict]] = dict()
path_to_hash: dict[str, str] = dict() # New index for O(1) path lookups
output_path = Path(output_dir / "song_hashes.json")
index_path = Path(output_dir / "path_to_hash.json")
# Load existing data
if output_path.exists():
with open(output_path, "r", encoding="utf-8") as f:
song_hashes = json.load(f, cls=DiffHashesDecoder)
if index_path.exists():
with open(index_path, "r", encoding="utf-8") as f:
path_to_hash = json.load(f)
saved_timestamp = 0.0
current_timestamp = time.time()
if (output_dir / 'timestamp.txt').exists():
with open(output_dir / 'timestamp.txt', 'r') as f:
saved_timestamp = float(f.read())
tja_paths = get_config()["paths"]["tja_path"]
all_tja_files: list[Path] = []
for root_dir in tja_paths:
root_path = Path(root_dir)
if (root_path / '.git').exists():
repo = Repo(root_path)
origin = repo.remotes.origin
origin.pull()
print('Pulled latest from', root_path)
all_tja_files.extend(root_path.rglob("*.tja"))
files_to_process = []
# O(n) pass to identify which files need processing
for tja_path in all_tja_files:
tja_path_str = str(tja_path)
current_modified = tja_path.stat().st_mtime
# Skip files that haven't been modified since last run
if current_modified <= saved_timestamp:
# File hasn't changed, just restore to global_data if we have it
current_hash = path_to_hash.get(tja_path_str)
if current_hash is not None:
global_data.song_paths[tja_path] = current_hash
continue
# O(1) lookup instead of nested loops
current_hash = path_to_hash.get(tja_path_str)
if current_hash is None:
# New file (modified after saved_timestamp)
files_to_process.append(tja_path)
else:
# File was modified after saved_timestamp, need to reprocess
files_to_process.append(tja_path)
# Clean up old hash
if current_hash in song_hashes:
del song_hashes[current_hash]
del path_to_hash[tja_path_str]
# Process only files that need updating
for tja_path in files_to_process:
tja_path_str = str(tja_path)
current_modified = tja_path.stat().st_mtime
tja = TJAParser(tja_path)
all_notes = deque()
all_bars = deque()
diff_hashes = dict()
for diff in tja.metadata.course_data:
diff_notes, _, diff_bars = TJAParser.notes_to_position(TJAParser(tja.file_path), diff)
diff_hashes[diff] = tja.hash_note_data(diff_notes, diff_bars)
all_notes.extend(diff_notes)
all_bars.extend(diff_bars)
if all_notes == []:
continue
hash_val = tja.hash_note_data(all_notes, all_bars)
if hash_val not in song_hashes:
song_hashes[hash_val] = []
song_hashes[hash_val].append({
"file_path": tja_path_str,
"last_modified": current_modified,
"title": tja.metadata.title,
"subtitle": tja.metadata.subtitle,
"diff_hashes": diff_hashes
})
# Update both indexes
path_to_hash[tja_path_str] = hash_val
global_data.song_paths[tja_path] = hash_val
# Save both files
with open(output_path, "w", encoding="utf-8") as f:
json.dump(song_hashes, f, indent=2, ensure_ascii=False)
with open(index_path, "w", encoding="utf-8") as f:
json.dump(path_to_hash, f, indent=2, ensure_ascii=False)
with open(output_dir / 'timestamp.txt', 'w') as f:
f.write(str(current_timestamp))
return song_hashes
def process_tja_file(tja_file):
"""Process a single TJA file and return hash or None if error"""
@@ -25,78 +147,6 @@ def process_tja_file(tja_file):
hash = tja.hash_note_data(all_notes[0], all_notes[2])
return hash
def build_song_hashes(output_file="cache/song_hashes.json"):
existing_hashes = {}
output_path = Path(output_file)
if output_path.exists():
try:
with open(output_file, "r", encoding="utf-8") as f:
existing_hashes = json.load(f)
except (json.JSONDecodeError, IOError) as e:
print(
f"Warning: Could not load existing hashes from {output_file}: {e}"
)
existing_hashes = {}
song_hashes = existing_hashes.copy()
tja_paths = get_config()["paths"]["tja_path"]
all_tja_files = []
for root_dir in tja_paths:
root_path = Path(root_dir)
if (root_path / '.git').exists():
repo = Repo(root_path)
origin = repo.remotes.origin
origin.pull()
print('Pulled latest from', root_path)
all_tja_files.extend(root_path.rglob("*.tja"))
updated_count = 0
for tja_file in all_tja_files:
current_modified = tja_file.stat().st_mtime
should_update = False
hash_val = None
existing_hash = None
for h, data in song_hashes.items():
if data["file_path"] == str(tja_file):
existing_hash = h
break
if existing_hash is None:
should_update = True
else:
stored_modified = song_hashes[existing_hash].get("last_modified", 0)
if current_modified > stored_modified:
should_update = True
del song_hashes[existing_hash]
if should_update:
tja = TJAParser(tja_file)
all_notes = []
for diff in tja.metadata.course_data:
all_notes.extend(
TJAParser.notes_to_position(TJAParser(tja.file_path), diff)
)
if all_notes == []:
continue
hash_val = tja.hash_note_data(all_notes[0], all_notes[2])
song_hashes[hash_val] = {
"file_path": str(tja_file),
"last_modified": current_modified,
"title": tja.metadata.title,
"subtitle": tja.metadata.subtitle,
}
updated_count += 1
with open(output_file, "w", encoding="utf-8") as f:
json.dump(song_hashes, f, indent=2, ensure_ascii=False)
print(f"Song hashes saved to {output_file}. Updated {updated_count} files.")
return song_hashes
def get_japanese_songs_for_version(csv_file_path, version_column):
# Read CSV file and filter rows where the specified version column has 'YES'
version_songs = []

View File

@@ -232,9 +232,9 @@ class GlobalData:
textures: dict[str, list[ray.Texture]] = field(default_factory=lambda: dict())
songs_played: int = 0
config: dict = field(default_factory=lambda: dict())
song_hashes: dict[str, list[dict]] = field(default_factory=lambda: dict()) #Hash to path
song_paths: dict[Path, str] = field(default_factory=lambda: dict()) #path to hash
global_data = GlobalData()
shader = ray.load_shader('', 'shader/outline.fs')
class OutlinedText:
def __init__(self, text: str, font_size: int, color: ray.Color, outline_color: ray.Color, outline_thickness=5.0, vertical=False):
@@ -260,7 +260,7 @@ class OutlinedText:
])
texture_size = ray.ffi.new("float[2]", [self.texture.width, self.texture.height])
self.shader = shader
self.shader = ray.load_shader('shader/outline.vs', 'shader/outline.fs')
outline_size_loc = ray.get_shader_location(self.shader, "outlineSize")
outline_color_loc = ray.get_shader_location(self.shader, "outlineColor")
texture_size_loc = ray.get_shader_location(self.shader, "textureSize")
@@ -279,7 +279,7 @@ class OutlinedText:
rotate_chars = {'-', '', '|', '/', '\\', '', '', '~', '', '', '(', ')',
'', '', '[', ']', '', '', '', '', '', '', '', ':', ''}
max_char_width = 0
total_height = padding * 2 # Top and bottom padding
total_height = padding * 2
for char in text:
if font:
@@ -288,22 +288,20 @@ class OutlinedText:
char_width = ray.measure_text(char, font_size)
char_size = ray.Vector2(char_width, font_size)
# If character should be rotated, swap width and height for measurements
if char in rotate_chars:
effective_width = char_size.y # Height becomes width when rotated 90°
effective_width = char_size.y
else:
effective_width = char_size.x
max_char_width = max(max_char_width, effective_width)
total_height += len(text) * font_size
width = int(max_char_width + (padding * 2)) # Add left and right padding
width = int(max_char_width + (padding * 2))
height = total_height
image = ray.gen_image_color(width, height, bg_color)
for i, char in enumerate(text):
char_y = i * ray.measure_text_ex(self.font, char, font_size, 0).y
char_y += padding
char_y = i * font_size + padding
if font:
char_size = ray.measure_text_ex(font, char, font_size, 0)
@@ -313,26 +311,22 @@ class OutlinedText:
char_size = ray.Vector2(char_width, font_size)
char_image = ray.image_text(char, font_size, color)
# Rotate character if it's in the rotate_chars set
if char in rotate_chars:
rotated_image = ray.gen_image_color(char_image.height, char_image.width, ray.BLANK)
# Manual 90-degree clockwise rotation
for y in range(char_image.height):
for x in range(char_image.width):
src_color = ray.get_image_color(char_image, x, y)
# 90° clockwise: new_x = old_y, new_y = width - 1 - old_x
new_x = y
new_y = char_image.width - 1 - x
new_x = char_image.height - 1 - y
new_y = x
ray.image_draw_pixel(rotated_image, new_x, new_y, src_color)
ray.unload_image(char_image)
char_image = rotated_image
effective_width = char_size.y # Height becomes width when rotated
effective_width = char_size.y
else:
effective_width = char_size.x
# Center the character horizontally
char_x = width // 2 - effective_width // 2
ray.image_draw(image, char_image,

View File

@@ -105,7 +105,7 @@ class EntryScreen:
self.selected_box = max(0, self.selected_box - 1)
if is_r_kat_pressed():
audio.play_sound(self.sound_kat)
self.selected_box = min(self.num_boxes, self.selected_box + 1)
self.selected_box = min(self.num_boxes - 1, self.selected_box + 1)
def update(self):
self.on_screen_start()

View File

@@ -40,9 +40,9 @@ class ResultScreen:
self.load_textures()
self.load_sounds()
self.screen_init = True
self.song_info = FontText(session_data.song_title, 40).texture
self.song_info = OutlinedText(session_data.song_title, 40, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5)
audio.play_sound(self.bgm)
self.fade_in = FadeIn(get_current_ms())
self.fade_in = FadeIn()
self.fade_out = None
self.gauge = None
self.score_delay = None
@@ -165,7 +165,9 @@ class ResultScreen:
ray.draw_texture(self.textures['result'][330], -5, 3, ray.WHITE)
ray.draw_texture(self.textures['result'][(global_data.songs_played % 4) + 331], 232, 4, ray.WHITE)
ray.draw_texture(self.song_info, 1252 - self.song_info.width, int(35 - self.song_info.height / 2), ray.WHITE)
src = ray.Rectangle(0, 0, self.song_info.texture.width, self.song_info.texture.height)
dest = ray.Rectangle(1252 - self.song_info.texture.width, 35 - self.song_info.texture.height / 2, self.song_info.texture.width, self.song_info.texture.height)
self.song_info.draw(src, dest, ray.Vector2(0, 0), 0, ray.WHITE)
ray.draw_texture(self.textures['result'][175], 532, 98, ray.fade(ray.WHITE, 0.75))
@@ -189,7 +191,7 @@ class ResultScreen:
class FadeIn:
def __init__(self, current_ms: float):
def __init__(self):
self.fadein = Animation.create_fade(450, initial_opacity=1.0, final_opacity=0.0, delay=100)
self.fade = ray.fade(ray.WHITE, self.fadein.attribute)
@@ -212,12 +214,6 @@ class FadeIn:
ray.draw_texture(texture_2, x, screen_height - texture_2.height + texture_2.height//2, self.fade)
x += texture_2.width
class FontText:
def __init__(self, text, font_size):
self.text = OutlinedText(str(text), font_size, ray.Color(255, 255, 255, 255), ray.Color(0, 0, 0, 255), outline_thickness=5)
self.texture = self.text.texture
class ScoreAnimator:
def __init__(self, target_score):
self.target_score = str(target_score)

View File

@@ -5,7 +5,6 @@ from typing import Optional, Union
import pyray as ray
from libs import song_hash
from libs.animation import Animation
from libs.audio import audio
from libs.tja import TJAParser
@@ -418,20 +417,8 @@ class SongBox:
def get_scores(self):
if self.tja is None:
return
with sqlite3.connect('scores.db') as con:
cursor = con.cursor()
diffs_to_compute = []
for diff in self.tja.metadata.course_data:
if diff not in self.hash:
diffs_to_compute.append(diff)
if diffs_to_compute:
for diff in diffs_to_compute:
notes, _, bars = TJAParser.notes_to_position(TJAParser(self.tja.file_path), diff)
self.hash[diff] = self.tja.hash_note_data(notes, bars)
# Batch database query for all diffs at once
if self.tja.metadata.course_data:
hash_values = [self.hash[diff] for diff in self.tja.metadata.course_data]
@@ -1004,17 +991,15 @@ class SongFile(FileSystemItem):
if self.is_recent:
self.tja.ex_data.new = True
title = self.tja.metadata.title.get(global_data.config['general']['language'].lower(), self.tja.metadata.title['en'])
self.hash = global_data.song_paths[path]
self.box = SongBox(title, texture_index, False, tja=self.tja, name_texture_index=name_texture_index if name_texture_index is not None else texture_index)
self.box.hash = global_data.song_hashes[self.hash][0]["diff_hashes"]
self.box.get_scores()
class FileNavigator:
"""Manages navigation through pre-generated Directory and SongFile objects"""
def __init__(self, root_dirs: list[str]):
# Handle both single path and list of paths
if isinstance(root_dirs, (list, tuple)):
self.root_dirs = [Path(p) if not isinstance(p, Path) else p for p in root_dirs]
else:
self.root_dirs = [Path(root_dirs) if not isinstance(root_dirs, Path) else root_dirs]
# Pre-generated objects storage
self.all_directories: dict[str, Directory] = {} # path -> Directory
@@ -1118,14 +1103,10 @@ class FileNavigator:
for i, tja_path in enumerate(sorted(tja_files)):
song_key = str(tja_path)
if song_key not in self.all_song_files:
try:
song_obj = SongFile(tja_path, tja_path.name, texture_index)
if song_obj.is_recent:
self.new_items.append(SongFile(tja_path, tja_path.name, 620, name_texture_index=texture_index))
self.all_song_files[song_key] = song_obj
except Exception as e:
print(f"Error creating SongFile for {tja_path}: {e}")
continue
content_items.append(self.all_song_files[song_key])
@@ -1302,19 +1283,20 @@ class FileNavigator:
hash_val, title, subtitle = parts[0], parts[1], parts[2]
original_hash = hash_val
if song_hash.song_hashes is not None:
if hash_val in song_hash.song_hashes:
file_path = Path(song_hash.song_hashes[hash_val]["file_path"])
if hash_val in global_data.song_hashes:
file_path = Path(global_data.song_hashes[hash_val][0]["file_path"])
if file_path.exists():
tja_files.append(file_path)
else:
# Try to find by title and subtitle
for key, value in song_hash.song_hashes.items():
if (value["title"]["en"] == title and
value["subtitle"]["en"][2:] == subtitle and
Path(value["file_path"]).exists()):
for key, value in global_data.song_hashes.items():
for i in range(len(value)):
song = value[i]
if (song["title"]["en"] == title and
song["subtitle"]["en"][2:] == subtitle and
Path(song["file_path"]).exists()):
hash_val = key
tja_files.append(Path(song_hash.song_hashes[hash_val]["file_path"]))
tja_files.append(Path(global_data.song_hashes[hash_val][i]["file_path"]))
break
if hash_val != original_hash:
@@ -1523,17 +1505,3 @@ class FileNavigator:
self.current_root_dir = Path()
self.history.clear()
self.load_root_directories()
def get_stats(self):
"""Get statistics about the pre-generated objects"""
song_count_by_dir = {}
for dir_path, items in self.directory_contents.items():
song_count_by_dir[dir_path] = len([item for item in items if isinstance(item, SongFile)])
return {
'total_directories': len(self.all_directories),
'total_songs': len(self.all_song_files),
'root_items': len(self.root_items),
'directories_with_content': len(self.directory_contents),
'songs_by_directory': song_count_by_dir
}

27
shader/outline.vs Normal file
View File

@@ -0,0 +1,27 @@
#version 330
// Input vertex attributes
in vec3 vertexPosition;
in vec2 vertexTexCoord;
in vec3 vertexNormal;
in vec4 vertexColor;
// Input uniform values
uniform mat4 mvp;
uniform mat4 matModel;
uniform mat4 matView;
uniform mat4 matProjection;
// Output vertex attributes (to fragment shader)
out vec2 fragTexCoord;
out vec4 fragColor;
void main()
{
// Calculate final vertex position
gl_Position = mvp * vec4(vertexPosition, 1.0);
// Send vertex attributes to fragment shader
fragTexCoord = vertexTexCoord;
fragColor = vertexColor;
}