Modernize and organize project to current .NET standards, make cross-platform, fix various bugs, and close functionality gap#5
Conversation
Modernize the project and improve cross-platform compatibility
Have the camera move more closely with the mouse
Loading previously ran the whole CCSFile read+init inside the render callback (on the UI thread), so parsing a large file froze the UI and stalled the animation. Split loading into a CPU-only parse phase that runs on a background Task and a GL-upload phase that stays on the render callback (the only place the GL context is current): - Scene.LoadCCSFile -> ReadCCSFile (parse, any thread) + InitCCSFile (GL). - EnqueueGlJob is now callable from any thread; it marshals the RequestNextFrameRendering wake-up to the UI thread. - MainWindow.LoadFiles parses on Task.Run, enqueues the GL init, then adds the tree node via the dispatcher; read failures are caught/logged. Because logging now happens off the UI thread, guard Logger's de-dup dictionaries with a lock and restructure AppendLog to marshal to the UI thread before echoing, so each message is written exactly once. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The windows were ported WinForms-style: each declared private fields
mirroring its controls, assigned them via this.FindControl<T>("name") in
the constructor, and defined a manual InitializeComponent that called
AvaloniaXamlLoader.Load. Avalonia's XAML compiler already generates
strongly-typed fields for every x:Name'd control plus InitializeComponent,
so all of that was redundant boilerplate with only runtime-checked names.
Drop the manual InitializeComponent methods and the ~36 FindControl
lookups across the four windows and reference the generated fields
directly. This also removes the AvGrid alias in MainWindow (the generated
fields are already typed, so the StudioCCS.Grid vs Avalonia.Controls.Grid
ambiguity no longer surfaces). No behavior change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The main window populated its TreeViews by recursively constructing TreeViewItems in code (BuildTreeItem/BuildSceneAnimeItem) and drove the View-menu toggles + status bar through imperative handlers (ApplyViewMenu, IsChecked lookups, a per-frame UpdateStatus). Move this to idiomatic Avalonia: - A light MainViewModel (ViewModelBase + INotifyPropertyChanged) exposes the tree data sources (CcsRoots / SceneRoots), the render-option toggles (which write straight through to the static Scene), and the status text. It's a thin shim over Scene, not a full MVVM layer. - The TreeViews bind ItemsSource to those collections and render via a shared compiled-binding TreeDataTemplate over CcsTreeNode; per-node-type context menus move to ContextRequested handlers (CcsTreeNode lives in the portable model and shouldn't carry UI command info). - View-menu items two-way bind IsChecked to the view-model; the status bar binds its text. A timer just refreshes the camera string. - CcsTreeNode.Nodes becomes an ObservableCollection so the scene tree updates when animations are added/removed after binding. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
StudioCCS.Grid (the static OpenGL grid-renderer helper) collided with Avalonia.Controls.Grid, which had forced a 'using AvGrid = ...' alias in the window code-behind. Rename the class (and its file) to GridRenderer so the name no longer shadows the Avalonia control. Only the three call sites in Scene.cs reference it; the "Grid" shader-file name and log strings are unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bring the bone editor in line with the rest of the UI and remove duplication that had accumulated across the windows: - EditBoneWindow now binds its TreeView to CcsTreeNodes (Tag = the bone) via ItemsSource instead of hand-building TreeViewItems, matching the CCS and scene trees. The BoneNodeTag wrapper is gone (the CCSObject is the Tag directly). - The CcsTreeNode TreeDataTemplate moves to App-level resources so all three trees share one definition rather than redeclaring it. - File-picker type descriptors (All / CCS / Bin) are centralized in a FileFilters helper, replacing three inline copies across the load, load-matrix, and pose dialogs. - The two ContextRequested handlers share helpers (ContextNode, Menu, OpenNodeMenu) so node extraction, menu construction (single IList cast), and pointer-placed opening are written once; pattern matching is standardized on 'is not'. No behavior change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Preview/Scene/All toggle was three ToggleButtons whose mutual exclusion was hand-maintained with a _suppressModeEvents reentrancy guard and manual IsChecked juggling in code-behind — the one imperative holdout in an otherwise VM-bound UI. Replace them with grouped RadioButtons (mutually exclusive by design) bound to a new MainViewModel.Mode property (with Is*Mode bool wrappers for the bindings). Mode is the single source of truth and writes through to Scene.SceneDisplay. Code-behind now only mirrors mode changes into the panel layout (kept there because the column GridLength collapse doesn't bind cleanly), via the view-model's PropertyChanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Scrolling up now zooms the camera in (toward the model) rather than out, which matches the common convention. Negated the wheel delta forwarded from the viewport to Scene.MouseWheel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Clicking a tree item's label now expands/collapses it, so users don't have to hit the small expand/collapse chevron. A shared TreeViewExpand helper handles the TreeView's Tapped event: it ignores taps on the chevron (which already toggles) and otherwise toggles IsExpanded on the tapped row if it has children. Wired to all three trees (CCS objects, scene animations, clump bones). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The orbit camera reconstructs its orientation with Matrix4.LookAt and a fixed +Y up-vector. At exactly +/-90 deg pitch the view direction aligns with that up-vector, the camera basis (cross(forward, up)) collapses, and the model and axis gizmo flip 180 deg (gimbal lock at the pole). Clamp pitch to +/-89.9 deg so the turntable never reaches the singular orientation: rotation stays smooth and level right up to a near-top-down view. 89.9 keeps an effectively straight-down view while staying well clear of float precision issues (cos(89.9 deg) ~= 0.0017). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Shaders (data/shaders/*.{vsh,fsh,gsh}) and the vertex blobs
(data/bin/*.bin) are now compiled in as AvaloniaResource items and read
at runtime through avares:// URIs via a new EmbeddedData helper, instead
of opening FileStream/StreamReader against files copied to the output
directory. This drops the data/**/* copy-to-output rule; only
blenderDummyImport.py is still copied out.
Since the data is now immutable and assembly-resident, the disk
reload-from-file paths no longer make sense: removed the unused
AxisMarker.Reload() and demoted WireHelper.ReadBin() to private (it has
only the single internal Init() caller).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Only the GL bindings and math types are used; Avalonia provides the window and GL context. Dropping the OpenTK metapackage removes the unused GLFW windowing native redist, audio, compute, and input packages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The old static Logger owned plumbing the framework already provides and had several design flaws: severity was encoded as a System.Drawing.Color (so the core had no log levels and couldn't filter), dedup keyed on string.GetHashCode() (collisions could silently drop distinct messages), the LogOnceCode half was dead, and caller info was captured then discarded. Adopt Microsoft.Extensions.Logging via a thin static facade (Log.Error/ Warning/Info) over a LoggerFactory configured once at startup. This matches the codebase's existing static-utility idiom (no DI container) while letting the framework own levels, filtering, formatting, and the stdout sink. What we still own is just the two things we actually care about: - PanelLoggerProvider: routes formatted output to the in-app log panel and maps LogLevel -> Color in the view layer (the only place Color now lives). - LogOnce: per-frame flood protection, keyed on the message string (fixes the GetHashCode collision risk). Replaces the old LogType machinery; the 4 render-loop call sites now pass once: true. Log.* also trims trailing newlines centrally, so each sink owns line termination (the framework console provider and the panel provider each append one) instead of every call site baking in "\n". Verified: app loads CCS files, GL context initializes, and info/warn/fail all emit through the console provider with correct level prefixes and no double-newlines (0 across 3526 log lines over a 30-file load). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The in-app log panel was a plain read-only TextBox: it ignored the colour
the provider passed and showed no severity, so it read as flat, level-less
text while stdout (via the console provider) was clearly tiered. Make the
panel a copy of stdout.
- PanelLoggerProvider now prefixes each line with the console's level
abbreviation ("info:/warn:/fail:" etc.) and emits one line per log call
(no trailing newline; the facade already trims). Severity colours are
brightened to read on a dark surface.
- The panel becomes a virtualizing ListBox of colour-coded lines (LogLine:
text + brush), keeping it responsive under heavy logging where the old
Text += concat was O(n^2). Backing collection is capped at 2000 lines,
oldest dropped first, and auto-scrolls to the newest.
- The panel background is fixed dark (not theme-dependent) so white/orange/
red stay legible regardless of the OS light/dark setting.
Verified live: loaded several CCS files and confirmed the panel renders
orange "warn:" and grey "info:" lines on the dark console, mirroring stdout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SimpleConsole formatter printed "info: StudioCCS[0] message" — the
category/eventId is constant and redundant in a single-app process, and it
made stdout diverge from the in-app log panel ("info: message").
Replace SimpleConsole with a small custom ConsoleFormatter that prints just
"<level>: <message>", ANSI-colouring the level tag only on a real terminal
(suppressed when stdout is redirected). The level abbreviation now comes from
a shared LogLevelTag helper used by both the console formatter and the panel
provider, so the two renderings can't drift apart.
Verified: stdout shows "info:/warn:/fail: <message>" with no "StudioCCS[0]"
and no stray ANSI escapes when redirected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The console and panel each had their own severity-colour table (console: ANSI yellow/green/red; panel: orange/grey/bright-red) and disagreed on scope (console coloured just the level tag, panel coloured the whole line). So even matching text rendered with different colours. Introduce LogPalette as the single source of truth for severity colours (RGB). The console formatter emits a 24-bit truecolor ANSI escape from it; the panel builds its brush from the same values — so the two are identical by construction. Both now colour only the severity tag: the panel template renders the tag in its colour and ": <message>" in a neutral light grey, matching the console (which resets colour before the message). Verified: panel shows an orange "warn:" tag with a neutral message, and a pty capture of stdout shows "\e[38;2;255;165;0mwarn\e[0m: ..." — same orange, tag only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GL profile was requested only through X11PlatformOptions, so the desktop OpenGL 3.3 (#version 330 + geometry shader) context the CCS shaders require was set up on Linux alone. On Windows the app fell back to ANGLE (OpenGL ES), which cannot compile those shaders. Choose the profile by platform from one shared desktop profile list: Windows switches to native WGL (RenderingMode=Wgl + WglProfiles), Linux keeps X11 GlProfiles. macOS (AvaloniaNative) exposes no GL-version selector, so it is documented as requiring on-device verification. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Encoding.Default is platform-dependent (Windows-1252 on Windows, UTF-8 elsewhere), so any name byte >= 0x80 decoded differently per OS. CCS asset names are ASCII; using Encoding.ASCII makes parsing deterministic across platforms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Store text files with LF in the repo while checking them out with each platform's native ending (CRLF on Windows, LF elsewhere), keeping history consistent without fighting local tooling. Mark known binary assets (.dds/.png/.bin/.ccs) so they are never normalized. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
texture2D was removed from GLSL in the 330 core profile; desktop Mesa and NVIDIA/AMD drivers accept it leniently, but Apple's strict core compiler rejects it, which would fail shader compilation on macOS. Switch the three active samplers to the overloaded texture() call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verify the negotiated context is desktop GL 3.2+ in OnOpenGlInit; if it is older, disable the viewport with a clear log message instead of binding shaders that never compiled and spamming GL errors into a black viewport every frame. The disabled state fills the viewport dark red and stops requesting frames rather than spinning. Also register a KHR_debug callback in debug builds (where the context is 4.3+) to surface driver warnings and errors through the existing log, making platform-specific rendering issues diagnosable instead of silent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The capability guard accepted 3.2, but the shaders are #version 330 and need GL 3.3, so a 3.2 context would pass the guard and then fail every shader compile - the same silent black viewport the guard exists to prevent. Raise the floor to 3.3 and drop the unusable 3.2 entry from the context negotiation list. 3.3 is available everywhere; macOS meets it with its 4.1 core context (it offers only 3.2 or 4.1, never 3.3). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Five shaders (CCSClump and Triangle) declared bare #version 330 while the rest used #version 330 core. The two are equivalent on a core context (330 defaults to core), so this is a consistency/intent change, not a behavioral one: every shader now explicitly states it targets the core profile and relies on no compatibility features. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A SharpDevelop/Windows-era toolchain had saved 46 source, shader, and doc files as UTF-8 with a leading BOM (EF BB BF). The marker is redundant for UTF-8, breaks naive first-line parsing (it hid shader #version lines from grep), and is out of place in a cross-platform repo. Remove the three BOM bytes from every affected file - content and LF line endings are otherwise untouched. Add a root .editorconfig pinning charset = utf-8 (no BOM) so Windows editors do not silently reintroduce it, complementing the existing .gitattributes line-ending normalization. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ask the WGL/X11 backends for OpenGL 4.6 (the highest version) first so capable Windows/Linux drivers also grant KHR_debug (core in 4.3), which lets the debug-build output callback actually engage. The list still falls back to 3.3 - the minimum the #version 330 shaders require - when a driver cannot provide anything newer. macOS is unaffected (AvaloniaNative ignores this list and caps at a 4.1 core context). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the render-info readout out of the full-width bottom strip and into the viewport column: render mode in a top bar, live camera state in a bottom bar, so the GL surface is sandwiched between them and the tree spans the full side. The mode bar gets the full viewport width since the list grows as more views are toggled on. Also reformat the camera readout with grouped axis labels, degree marks, and fixed-width fields rendered in a monospace font so it stays legible and stops jittering as it refreshes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Fluent-themed controls already tracked the OS light/dark setting, but the GL viewport (clear colour + grid lines, set in raw GL) and the Mode/ Camera status bars (hardcoded #202020) stayed dark regardless, which looked out of place against a light UI. Drive the viewport clear/grid colours from Scene.BackgroundColor/GridColor, re-applied every frame and updated on ActualThemeVariantChanged so an OS theme switch is reflected live. Move the status-bar palette into theme-keyed ResourceDictionaries (DynamicResource) so the bars re-resolve on a switch. The dark values are unchanged; only a light-mode variant is added. The log panel stays fixed-dark by design (its severity colours need a dark ground). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With thousands of log lines, the log ListBox's VirtualizingStackPanel throws "Invalid Arrange rectangle" from ScrollIntoView's forced layout pass, aborting the whole app at extreme file counts. It fired from two distinct triggers, so fix both to close the class of crash: - Our own auto-scroll to the newest line in FlushLogs runs back-to-back during a heavy parse; wrap it and swallow the transient InvalidOperationException. The scroll is cosmetic, so skipping one during extreme churn is harmless. - The ListBox's built-in auto-scroll-to-selected runs ScrollIntoView during a layout reflow (e.g. switching to the All view) and is posted by the framework, so it can't be wrapped at our call site. Disable it with AutoScrollToSelectedItem="False"; we already scroll to the newest line ourselves, and selecting/copying lines still works. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three stray Console.WriteLine("Wtf 1") calls sat in CCSHitMesh.RenderAll, so
they printed every frame for every hit group — flooding stdout and adding
needless console I/O on the render thread once any hit-mesh is on screen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Preview/Scene/All radio group sat on its own row below the menu, leaving a wide empty band beside the menu. Put both in a top DockPanel: the radio group docks Right (anchored to the window edge) and the Menu fills the rest of the row. A left margin on the radio group keeps a minimum gap from the menu items. This reclaims the wasted space and gives the viewport a little more height. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CCSFile.Init allocates GL resources for the hit, clump, texture, and bbox sections, but DeInit only freed the hit and bbox ones. So every unload leaked all of a file's texture handles, its clumps' matrix buffers/textures and bone VAOs/VBOs, and the ref-counted shared clump shader program (ProgramRefs never reached zero, so the program was never deleted). Mirror Init and DeInit the clumps and textures too. Their DeInit methods already existed and are correct (the program is ref-counted and recreated on demand via the ProgramID == -1 guard); they simply were never called. Fixes the leak for both the per-file Unload and the upcoming Unload All. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Right-clicking a loaded file in the tree now offers "Unload All" alongside the per-file "Unload", clearing every loaded CCS file without restarting the program. UnloadAllCCSFiles DeInits each file (freeing its GL resources), clears the file list, and drops the scene state that points back into the files - the active animations and the preview selection - so nothing dereferences freed objects afterwards. It runs as a single render-thread job (GL context current), and the bound trees are cleared on the UI thread. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bottom camera readout was one monospace string, padded with spaces to keep it from jittering as it refreshes ~10x/sec. Replace that with a proper layout: split CameraStatus into per-value properties (rotation, target, distance) and place each in a fixed-width, right-aligned box so the columns stay put without a monospace font. Labels and values now use the normal face in the dark value colour at SemiBold, matching the top Mode bar. The gap after "Camera" matches the Mode bar's, and the Rot/Target/Dist groups are separated by margin (wider than a value box, so each group's last value reads as part of its group rather than the next label). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
I took this a bit further and loaded all 1023 CCS files from .hack//INFECTION and the program no longer crashes, it remains responsive, and it does so pretty quickly. Though, I'm not in love with the logging panel. It seems to be causing problems with the current implementation. This might need to be redone. |
|
Regarding the drag and drop bits being broken. It's not a problem in our code, rather, a problem in Avalonia on Linux:
If this stays on track, it's targeted for Avalonia 12.1. This should work just fine on Windows, but I'll need to test. |
A thin bar docked at the bottom of the window, shown only while a file-load batch is in flight. It advances one step as each file finishes fully loading (GL upload done, or parse failed/skipped), so it tracks real completion rather than parse start. Counts are cumulative across overlapping batches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The render callback ran the entire queued-GL-job queue each frame and only then called Scene.Render(); since the callback runs on the UI thread, a large load batch blocked it for seconds at a stretch. Spend at most ~8ms on jobs per frame and let the continuous redraw loop drain the rest on following frames. At least one job always runs per frame to guarantee forward progress. Impact beyond UI chrome staying responsive: - The 3D viewport keeps rendering during a load. Previously Scene.Render() did not run until the whole queue cleared, so the viewport froze - no camera orbit, animation playback, or grid redraw - until the load finished. Now it stays live throughout. - Progressive visual feedback: frames render between job batches, so models pop into the scene as they finish uploading instead of all appearing at the end. - Bounded UI-thread occupancy for everything competing during a heavy load (menus, resize, the log panel, theme changes), not just the load itself. This helps work that is split into many jobs (one per file on load). It does not subdivide a single monolithic job, so the unload paths - enqueued as one job - are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A four-digit value plus ".x" overflows the fixed-width status boxes, so every numeric value in the camera bar now keeps one decimal until its magnitude hits 1000 and drops it after. Shared via FormatCameraValue so the rotations, targets, and distance all behave identically. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A wheel notch added a fixed distance, so it moved the view ~1% at distance 10 but only ~0.1% at distance 100 - far-away zoom crawled. Scaling the step by Distance/Reference turns the constant additive step into a constant multiplicative one, so each notch is the same percentage change at any zoom level. Applied to both the wheel and keyboard zoom. WASD/XZ panning had the same problem - a fixed target step is a tiny fraction of the view when zoomed out, so panning crawled. It now scales by the same distance factor, so a pan moves a constant fraction of the view at any zoom level. The shared anchor is CameraDistanceReference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Centralize the default framing in ArcBallCamera.Reset() so construction and the reset action share one definition and can't drift apart. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the R-key camera reset, the three viewport modes (Preview/Scene/ All), the View-menu display toggles, and the Default to Axis Movement option. Correct stale claims surfaced by cross-referencing the code: bounding boxes and dummies now render in All mode, clicking an animation previews its first frame, the export menu item is "Dump to .OBJ...", and Load Matrix loads a per-bone 4x4 matrix dump. Also fix the companion script path and an ambiguous Unicode character. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DeInit looped over every node calling DeInit() unconditionally, but GetObject<CCSObject> returns null for non-object nodes (most commonly effect nodes), causing a NullReferenceException when unloading files that contain such clumps. This surfaced when unloading the full asset set after loading all 1023 assets. Rework the loop to mirror Init: branch on node type and DeInit both SECTION_OBJECT and SECTION_EFFECT nodes via their correct typed lookups, each null-guarded. Behavior-neutral today (CCSEffect.DeInit is a no-op) but restores the Init/DeInit symmetry the teardown contract relies on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shader-uniform guard in CCSClump.Init checked UniformMatrix twice and never validated UniformMatrixList, so a missing uMatrixList uniform would slip through unreported. Check the correct location and fix the log label to match the value it prints. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CCSClump.Init created the static shader program, then fetched its attribute/uniform locations, and bumped the shared ProgramRefs counter only afterwards. If an attribute or uniform lookup failed in between, Init returned with ProgramID left pointing at a live program that was never ref-counted - leaking it and desyncing the static accounting from the set of clumps actually holding the program. Delete the program and reset ProgramID to -1 on those failure paths, so the invariant holds: ProgramID is set only when the program was fully acquired, and every clump that increments ProgramRefs holds a valid, matching program. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Init allocates incrementally - shader program ref, BoneVis VAO/VBO, matrix buffer and texture, then Init's each child node - but its one post-allocation failure path (a null effect node) just returned false. Because a file whose Init fails is discarded without ever being added to the scene's file list, DeInit never runs on it, so every GL object this clump (and any fully-init'd sibling clump in the same file) had already allocated was leaked. Extract the teardown DeInit was doing into DeInitNode/ReleaseClumpResources helpers and call them on the failure path to roll back this clump's own GL resources, release its ProgramRefs hold, and DeInit the nodes that were actually initialized (indices before the failure). Nodes at/after the failure were never Init'd and are deliberately left untouched, since DeInit'ing them would corrupt CCSModel's own static ref counts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump the fixed value-box width from 40 to 44 and scale the Target/Dist label left margins from 50 to 55 to preserve the 1.25 width-to-margin relationship. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The last thing I want to do is rework the logging panel a bit, then we can do a review of this for merging. I would like to add the ability to unpack data.bin files as well, so StudioCCS becomes a one-stop-shop for all things CCS, but that can be done in a separate PR. |
A heavy load (all 1023 CCS files from .hack//INFECTION) brought the log panel's accumulated issues together: a garbled last line, no light/dark theming, and a pair of crash workarounds. The garbled last line and the earlier "Invalid Arrange rectangle" crash were the same root cause - ScrollIntoView forcing a synchronous layout pass through the virtualizing panel. The try/catch from 42c786a stopped the crash but left the panel half-arranged, which is the text-on-text corruption. So drop ScrollIntoView entirely and tail the newest line by setting the ScrollViewer offset, which the panel resolves on its normal layout pass - no forced reflow to crash or corrupt. A pinned-to-bottom flag keeps the view glued to the tail unless the user scrolls up to read, and re-evaluates only on a genuine offset move so an incoming batch growing the extent isn't mistaken for the user scrolling away. Both hacks are gone; AutoScrollToSelectedItem stays off as the correct UX for a log rather than as a crash bandaid. Theme the panel: the background reuses StatusBarBackground so it matches the bars around the viewport, and the severity colours come from theme-aware Log* resources (dark mirrors the old fixed look; light gets darker text and deeper amber/red so info text and tags stay legible). LogLine now carries the severity level and the tag colour is driven by warn/error style classes, so a theme switch recolours every line live and no brush is marshalled in with the text. Extract the buffer and the thread-safe, coalesced hand-off into a new LogConsoleModel, so all of the panel's threading lives in one place instead of inline in the window. It uses AvaloniaList so each burst is one range update rather than N CollectionChanged events plus N RemoveAt(0) shifts, and splits interior newlines so every row stays single-line (uniform height, which the virtualization and tail math both rely on). The System.Drawing.Color plumbing and the view's brush cache are gone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LoadMatrix, OnSavePose and OnLoadPose called into the binary parse/write helpers directly after the file-picker await, so a malformed or unreadable file threw on an async-void handler and crashed the app rather than logging a failure. Wrap each in try/catch that reports via Log.Error, the same way the main CCS load path already handles a bad file. LoadMatrixList and LoadPose also read straight into the live FinalMatrixList / pose / bind arrays as they parsed, so a truncated file threw partway through and left the clump half-overwritten (and still rendered). Parse into a local buffer and commit to the live arrays only after the whole file reads cleanly, so a bad file now leaves the existing matrices/pose intact.
Util.NonExistantNode flags a missing object reference by setting the node's ForeColor to red, but the shared CCSNodeTemplate only bound Text, so the port silently dropped that cue and broken references rendered like any other node. Surface it via an IsMissing flag on CCSTreeNode that toggles a missingNode style class, coloured from a new theme-aware TreeNodeMissing brush - the same class-driven, theme-tracking pattern the log panel uses, so normal nodes keep the default foreground and only flagged ones turn red.
- LogPalette: correct the doc comment, which claimed the log panel reuses these RGB values; the panel actually has its own theme-aware Log* brushes (App.axaml) and LogPalette is console-only. - App.axaml: drop the StatusBarMono brushes (both themes), referenced nowhere. - MainWindow.axaml: drop the unused viewportHost x:Name. - EditBoneWindow: remove a leftover Debug.WriteLine (and its now-unused using), and replace two ad-hoc deg<->rad constants (0.0174533f, 3.14159265f) with OpenTK's MathHelper.DegreesToRadians/RadiansToDegrees.
The three .OBJ/.SMD export handlers called Scene.Dump* directly after the dialog await, so any file-I/O failure (bad path, permission denied, disk full) surfaced as an unhandled exception on an async-void handler and took the whole app down. Route all three through a RunExport helper that catches and logs the failure, matching the care the file-load path already takes. Kept synchronous on the UI thread on purpose: the dump mutates shared scene state (each clump's FrameForward advances/recomputes its pose) that the render loop reads every frame, so it must stay serialized with rendering rather than race it on a background thread.
The GL viewport re-armed itself unconditionally every frame, so it drew at the display refresh rate forever - pinning a core/GPU even on a static, idle view. Replace that with an on-demand model: - Scene gains a RequestRedraw() signal (a static RedrawRequested event) that GlViewport subscribes to over its visual-tree lifetime, plus a WantsContinuousRender() predicate (a held camera key or a playing animation). The render callback now re-arms only while GL jobs remain or WantsContinuousRender() is true, and otherwise idles until woken. - Triggers live at the mutation sites: the camera input methods and AddAnime/RemoveAnime raise RequestRedraw directly; discrete UI changes (render-option/mode toggles via the VM, tree selection, theme switch, the Set Pose / Load Matrix actions, and every bone-editor edit) raise it from their handlers. GlViewport also wakes on EffectiveViewportChanged so a resize repaints. This also lays the groundwork for suspending rendering (next commit): the render callback now has a single, explicit decision point for whether to draw and re-arm. Two consequences of no longer drawing every frame are handled here too: - DeltaTime is the wall-clock gap between Render() calls and scales camera motion/animation stepping, so a long idle would make the first frame after a wake lurch by the whole elapsed gap. Clamp it to a sane maximum so a wake steps once. - A camera key is cleared only by its KeyUp, which a control receives only while focused; a key held while the viewport loses focus would otherwise keep WantsContinuousRender() true forever and never let the loop idle. Release all held keys on viewport LostFocus / window deactivation. The predicate errs toward rendering, so a missed case is a wasted frame, not a frozen viewport. Needs runtime verification across the interactions (camera drag/keys, animation playback, option toggles, bone editing).
Building on render-on-demand: exports no longer freeze the app. RunExport runs the dump on a background thread (Task.Run) while a modal BusyDialog is shown, and guards the scene state the dump reads so nothing can mutate it mid-export. The dump mutates shared scene state (each clump's FrameForward advances and recomputes its pose) that the render loop reads every frame and the bone editor writes, so both have to be kept off it for the duration: - Scene.ExportInProgress is set while the dump runs. The render callback skips all scene work (Scene.Render and the GL-job drain) while it's set, so the render loop can't race the export's FrameForward mutations; it idles until the flag clears and a redraw is requested. - The BusyDialog is modal over MainWindow, which blocks the menu/tree export and edit actions and gives feedback; the user can't dismiss it (no decorations, OnClosing cancels). But it does NOT disable EditBoneWindow, which is a separate, unowned top-level window - so the same ExportInProgress flag also makes the bone editor refuse edits, closing the one mutation path the modal misses. The flag is raised and the dialog shown inside the try, so the finally always clears the flag and closes the dialog even if showing it throws - otherwise a failure there would leave the viewport suspended with no way to recover. The export dialog's getters read its controls, so the chosen path/options are snapshotted on the UI thread before the background dump; reading them inside the Task.Run lambda would touch the controls from the wrong thread. A bonus of off-threading: the dump's own progress logging now appears in the log panel live instead of all at once after the freeze.
Exporting to OBJ with "Dump animations to text" segfaulted the whole app. CCSAnime.DumpToText walks every controller across every frame and called CCSClump.BindMatrixList() each time; DumpPreviewToSMD (debug-only) did the same. BindMatrixList is a pure GPU upload (GL.BufferData of the matrix texture buffer) - the text/SMD dumps never read it, they read the controllers' CPU-side keyframe tracks, so the call contributed nothing to the output. Since exports now run on a background thread (no current GL context), that stray GL call faulted in the native driver - a SIGSEGV, not a managed exception, so RunExport's try/catch could not catch it and the process died. The base OBJ/SMD dump paths are already GL-free (FrameForward touches only managed arrays); these two animation paths were the only ones still touching GL off-thread. Drop both calls. Output is unchanged and the dump no longer depends on a GL context, and it skips a pile of needless per-frame uploads. The remaining BindMatrixList callers (CCSClump.Render, CCSAnime.Render) run inside the render callback where the context is current and are untouched.
|
Yeah, I was thinking about DATA.BIN packing and unpacking as a feature. The unpacking into a single directory is pretty easy. The tables in the elf files for them need to be changed if the file sizes change too much when repacking, but it wouldn't be too hard to scan the elf files to figure out where the tables are and patch them. |
Let me know if you want to change anything. This incorporates changes from @Casuallynoted, @taarna23, as well as my own. It fixes #3 and #4, wires up bounding box logic, gets the project to run natively cross-platform with modern .NET, and aligns repo artifacts and organization to current .NET standards.