mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-07-18 01:59:19 +02:00
The previous nearest-first build captured every deferred visual node as text into a sorted vector, which does not scale: one tomaszewo flora file alone holds 440k model nodes (the scenery has 1M+), so the capture ran the process to ~7 GB and its enumeration never finished. Stream the visual nodes in camera-distance rings instead, with no per-node capture (O(1) memory). The visual pass replays the twin once per ring (nearest first); a node is built only when its squared distance to the camera -- sampled once when the visual phase starts, so the partition is stable across passes -- falls in the current ring, otherwise the rest of its body is skipped in O(1) by jumping over the v6 marker span. Each node is therefore built exactly once, in roughly nearest-first order, through the normal node path (instancing buckets unchanged). Explicit triangles/lines shapes have no single position to ring-test by, so they build in the nearest ring pass only. Reader gains skip_to_node_end() (remembers the served node's end and jumps the cursor there); cParser::skipReplayNode() delegates it down the active include child. Verified: td.scn builds 4 rings, no duplicates, no unexpected tokens, complete ~1s after the infrastructure pass. tomaszewo stays memory-bounded (no OOM, no duplicates) where the capture approach previously hung. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
434 lines
15 KiB
C++
434 lines
15 KiB
C++
/*
|
|
This Source Code Form is subject to the
|
|
terms of the Mozilla Public License, v.
|
|
2.0. If a copy of the MPL was not
|
|
distributed with this file, You can
|
|
obtain one at
|
|
http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
#include "stdafx.h"
|
|
#include "scene/scenerybinary.h"
|
|
|
|
#include "scene/sn_utils.h"
|
|
#include "utilities/utilities.h" // ToLower
|
|
#include "utilities/Logs.h"
|
|
|
|
#include <unordered_map>
|
|
#include <thread>
|
|
#include <mutex>
|
|
#include <condition_variable>
|
|
#include <queue>
|
|
#include <functional>
|
|
#include <fstream>
|
|
#include <string_view>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
|
|
namespace scene {
|
|
|
|
namespace {
|
|
|
|
// LEB128 unsigned varint: 1 byte for values < 128 (covers most string-table indices),
|
|
// keeping keyword/index references compact.
|
|
void write_varint( std::ostream &Output, std::uint64_t Value ) {
|
|
do {
|
|
std::uint8_t byte = Value & 0x7F;
|
|
Value >>= 7;
|
|
if( Value != 0 ) { byte |= 0x80; }
|
|
Output.put( static_cast<char>( byte ) );
|
|
} while( Value != 0 );
|
|
}
|
|
|
|
// zig-zag maps small-magnitude signed integers to small unsigned varints
|
|
std::uint64_t zigzag_encode( std::int64_t Value ) {
|
|
return ( static_cast<std::uint64_t>( Value ) << 1 ) ^ static_cast<std::uint64_t>( Value >> 63 );
|
|
}
|
|
std::int64_t zigzag_decode( std::uint64_t Value ) {
|
|
return static_cast<std::int64_t>( ( Value >> 1 ) ^ ( ~( Value & 1 ) + 1 ) );
|
|
}
|
|
|
|
// buffer cursor reads (little-endian, matching sn_utils), bounds-checked: on overrun the
|
|
// cursor is parked at the end and zero is returned, so a truncated twin decodes to empty
|
|
// rather than reading out of bounds.
|
|
std::uint64_t read_varint( char const *&Cursor, char const *End ) {
|
|
std::uint64_t value = 0;
|
|
int shift = 0;
|
|
while( ( Cursor < End ) && ( shift < 64 ) ) {
|
|
std::uint8_t const byte = static_cast<std::uint8_t>( *Cursor++ );
|
|
value |= ( static_cast<std::uint64_t>( byte & 0x7F ) << shift );
|
|
if( ( byte & 0x80 ) == 0 ) { break; }
|
|
shift += 7;
|
|
}
|
|
return value;
|
|
}
|
|
std::uint32_t read_u32le( char const *&Cursor, char const *End ) {
|
|
if( End - Cursor < 4 ) { Cursor = End; return 0; }
|
|
auto const *b = reinterpret_cast<std::uint8_t const *>( Cursor );
|
|
Cursor += 4;
|
|
return ( std::uint32_t( b[ 0 ] ) ) | ( std::uint32_t( b[ 1 ] ) << 8 )
|
|
| ( std::uint32_t( b[ 2 ] ) << 16 ) | ( std::uint32_t( b[ 3 ] ) << 24 );
|
|
}
|
|
float read_f32le( char const *&Cursor, char const *End ) {
|
|
if( End - Cursor < 4 ) { Cursor = End; return 0.f; }
|
|
std::uint32_t const v = read_u32le( Cursor, End );
|
|
float f; std::memcpy( &f, &v, 4 ); return f;
|
|
}
|
|
double read_f64le( char const *&Cursor, char const *End ) {
|
|
if( End - Cursor < 8 ) { Cursor = End; return 0.0; }
|
|
auto const *b = reinterpret_cast<std::uint8_t const *>( Cursor );
|
|
Cursor += 8;
|
|
std::uint64_t v = 0;
|
|
for( int i = 0; i < 8; ++i ) { v |= ( static_cast<std::uint64_t>( b[ i ] ) << ( 8 * i ) ); }
|
|
double d; std::memcpy( &d, &v, 8 ); return d;
|
|
}
|
|
|
|
// on-disk entry tag, packed into the low 3 bits of the per-entry head varint
|
|
enum : std::uint64_t {
|
|
TAG_TOKEN = 0, // head >> 3 == interned string index
|
|
TAG_INCLUDE = 1,
|
|
TAG_INT = 2, // followed by a zig-zag varint
|
|
TAG_F32 = 3, // followed by 4 little-endian bytes
|
|
TAG_F64 = 4, // followed by 8 little-endian bytes
|
|
TAG_QTOKEN = 5, // quoted token; head >> 3 == interned string index
|
|
TAG_NODE = 6, // node marker: followed by varint(class) + varint(byte span)
|
|
TAG_BITS = 3,
|
|
TAG_MASK = 0x7,
|
|
};
|
|
|
|
// node class stored in the TAG_NODE marker
|
|
enum : std::uint64_t {
|
|
NODECLASS_INFRA = 0,
|
|
NODECLASS_VISUAL = 1,
|
|
};
|
|
|
|
// writes a numeric value in the most compact lossless-enough form: integral values as a
|
|
// zig-zag varint, otherwise f32 when it represents the value with negligible error,
|
|
// otherwise full f64.
|
|
void write_number( std::ostream &Output, double Value ) {
|
|
double integral = 0.0;
|
|
if( ( std::modf( Value, &integral ) == 0.0 )
|
|
&& ( Value >= -9.007199254740992e15 ) && ( Value <= 9.007199254740992e15 ) ) {
|
|
write_varint( Output, TAG_INT );
|
|
write_varint( Output, zigzag_encode( static_cast<std::int64_t>( Value ) ) );
|
|
return;
|
|
}
|
|
float const f = static_cast<float>( Value );
|
|
bool const f32ok = std::isfinite( f )
|
|
&& ( ( static_cast<double>( f ) == Value )
|
|
|| ( std::abs( static_cast<double>( f ) - Value ) <= 1e-6 * std::abs( Value ) ) );
|
|
if( f32ok ) {
|
|
write_varint( Output, TAG_F32 );
|
|
sn_utils::ls_float32( Output, f );
|
|
}
|
|
else {
|
|
write_varint( Output, TAG_F64 );
|
|
sn_utils::ls_float64( Output, Value );
|
|
}
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
std::uint32_t
|
|
scenery_binary_writer::intern( std::string const &Text ) {
|
|
auto const it = m_lookup.find( Text );
|
|
if( it != m_lookup.end() ) { return it->second; }
|
|
auto const index = static_cast<std::uint32_t>( m_table.size() );
|
|
m_lookup.emplace( Text, index );
|
|
m_table.emplace_back( Text );
|
|
return index;
|
|
}
|
|
|
|
std::ostream &
|
|
scenery_binary_writer::sink() {
|
|
return ( m_innode ? m_nodebuf : m_entries );
|
|
}
|
|
|
|
void
|
|
scenery_binary_writer::add_token( std::string const &Token, bool Quoted ) {
|
|
auto const tag = ( Quoted ? TAG_QTOKEN : TAG_TOKEN );
|
|
write_varint( sink(), ( static_cast<std::uint64_t>( intern( Token ) ) << TAG_BITS ) | tag );
|
|
++m_count;
|
|
}
|
|
|
|
void
|
|
scenery_binary_writer::add_number( double Value ) {
|
|
write_number( sink(), Value );
|
|
++m_count;
|
|
}
|
|
|
|
void
|
|
scenery_binary_writer::add_include( std::vector<std::string> const &Fileexpr, std::vector<std::string> const &Params ) {
|
|
auto &out = sink();
|
|
write_varint( out, TAG_INCLUDE );
|
|
write_varint( out, Fileexpr.size() );
|
|
for( auto const &token : Fileexpr ) { write_varint( out, intern( token ) ); }
|
|
write_varint( out, Params.size() );
|
|
for( auto const ¶m : Params ) { write_varint( out, intern( param ) ); }
|
|
++m_count;
|
|
}
|
|
|
|
void
|
|
scenery_binary_writer::begin_node() {
|
|
// start buffering this node's entries; end_node() emits the marker + buffered bytes
|
|
m_nodebuf.str( std::string() );
|
|
m_nodebuf.clear();
|
|
m_innode = true;
|
|
}
|
|
|
|
void
|
|
scenery_binary_writer::end_node( bool Visual ) {
|
|
if( false == m_innode ) { return; }
|
|
m_innode = false;
|
|
auto const body = m_nodebuf.str();
|
|
write_varint( m_entries, TAG_NODE );
|
|
write_varint( m_entries, Visual ? NODECLASS_VISUAL : NODECLASS_INFRA );
|
|
write_varint( m_entries, body.size() );
|
|
m_entries.write( body.data(), static_cast<std::streamsize>( body.size() ) );
|
|
}
|
|
|
|
bool
|
|
scenery_binary_writer::write( std::ostream &Output, scenery_file_kind Kind ) const {
|
|
|
|
// header: magic + kind + flags
|
|
sn_utils::ls_uint32( Output, SCENERYBINARY_MAGIC );
|
|
sn_utils::s_uint8( Output, static_cast<std::uint8_t>( Kind ) );
|
|
sn_utils::ls_uint32( Output, 0 ); // flags, reserved
|
|
|
|
// string table: count, then each string as varint(length) + raw bytes (so the
|
|
// reader can take views into the buffer without scanning for terminators)
|
|
write_varint( Output, m_table.size() );
|
|
for( auto const &text : m_table ) {
|
|
write_varint( Output, text.size() );
|
|
Output.write( text.data(), static_cast<std::streamsize>( text.size() ) );
|
|
}
|
|
|
|
// entries: the pre-encoded entry bytes (heads + payloads + node markers) verbatim,
|
|
// running to end-of-file (the reader streams until the buffer is exhausted)
|
|
auto const encoded = m_entries.str();
|
|
Output.write( encoded.data(), static_cast<std::streamsize>( encoded.size() ) );
|
|
|
|
return ( false == Output.fail() );
|
|
}
|
|
|
|
bool
|
|
scenery_binary_reader::open( std::string_view Buffer ) {
|
|
|
|
m_table.clear();
|
|
m_begin = m_cursor = m_end = nullptr;
|
|
m_size = 0;
|
|
|
|
char const *cursor = Buffer.data();
|
|
char const *const end = Buffer.data() + Buffer.size();
|
|
|
|
if( end - cursor < 9 ) { return false; } // magic(4) + kind(1) + flags(4)
|
|
if( read_u32le( cursor, end ) != SCENERYBINARY_MAGIC ) {
|
|
// unrecognized type or incompatible version
|
|
return false;
|
|
}
|
|
m_kind = static_cast<scenery_file_kind>( static_cast<std::uint8_t>( *cursor++ ) );
|
|
read_u32le( cursor, end ); // flags, reserved
|
|
|
|
// string table: views into the buffer (no copies)
|
|
auto tablesize = read_varint( cursor, end );
|
|
m_table.reserve( tablesize );
|
|
while( ( tablesize-- != 0 ) && ( cursor < end ) ) {
|
|
auto const len = read_varint( cursor, end );
|
|
if( static_cast<std::uint64_t>( end - cursor ) < len ) { cursor = end; break; }
|
|
m_table.emplace_back( cursor, static_cast<std::size_t>( len ) );
|
|
cursor += len;
|
|
}
|
|
|
|
// entries run from here to end-of-buffer
|
|
m_begin = m_cursor = cursor;
|
|
m_end = end;
|
|
m_size = end - cursor;
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
scenery_binary_reader::next( scenery_entry_view &Out ) {
|
|
|
|
auto const resolve = [ this ]( std::uint64_t index ) -> std::string_view {
|
|
return ( index < m_table.size() ) ? m_table[ static_cast<std::size_t>( index ) ] : std::string_view();
|
|
};
|
|
|
|
// skip node markers (and whole nodes not belonging to the current pass) until a
|
|
// servable entry is reached
|
|
std::uint64_t head = 0;
|
|
std::uint64_t tag = 0;
|
|
for( ;; ) {
|
|
if( m_cursor >= m_end ) { return false; }
|
|
head = read_varint( m_cursor, m_end );
|
|
tag = head & TAG_MASK;
|
|
if( tag != TAG_NODE ) { break; }
|
|
auto const cls = read_varint( m_cursor, m_end );
|
|
auto const span = read_varint( m_cursor, m_end );
|
|
bool const process =
|
|
( m_pass == scenery_load_pass::all )
|
|
|| ( ( m_pass == scenery_load_pass::infrastructure ) && ( cls == NODECLASS_INFRA ) )
|
|
|| ( ( m_pass == scenery_load_pass::visual ) && ( cls == NODECLASS_VISUAL ) );
|
|
if( false == process ) {
|
|
// skip the whole node body
|
|
m_cursor += static_cast<std::ptrdiff_t>( span );
|
|
if( m_cursor > m_end ) { m_cursor = m_end; }
|
|
}
|
|
else {
|
|
// remember where this node ends so the consumer can bail out of it in O(1)
|
|
// (skip_to_node_end) after peeking just its first few entries -- used by the
|
|
// camera-ring visual load to drop a node that's outside the current distance ring
|
|
m_nodeend = m_cursor + static_cast<std::ptrdiff_t>( span );
|
|
if( m_nodeend > m_end ) { m_nodeend = m_end; }
|
|
}
|
|
// when processing, fall through: the loop re-reads and decodes the node's first entry
|
|
}
|
|
|
|
Out.fileexpr.clear();
|
|
Out.params.clear();
|
|
|
|
switch( tag ) {
|
|
case TAG_INCLUDE: {
|
|
Out.type = scenery_entry_type::include;
|
|
auto fe = read_varint( m_cursor, m_end );
|
|
Out.fileexpr.reserve( fe );
|
|
while( ( fe-- != 0 ) && ( m_cursor < m_end ) ) { Out.fileexpr.emplace_back( resolve( read_varint( m_cursor, m_end ) ) ); }
|
|
auto pe = read_varint( m_cursor, m_end );
|
|
Out.params.reserve( pe );
|
|
while( ( pe-- != 0 ) && ( m_cursor < m_end ) ) { Out.params.emplace_back( resolve( read_varint( m_cursor, m_end ) ) ); }
|
|
break;
|
|
}
|
|
case TAG_INT:
|
|
Out.type = scenery_entry_type::number;
|
|
Out.number = static_cast<double>( zigzag_decode( read_varint( m_cursor, m_end ) ) );
|
|
break;
|
|
case TAG_F32:
|
|
Out.type = scenery_entry_type::number;
|
|
Out.number = static_cast<double>( read_f32le( m_cursor, m_end ) );
|
|
break;
|
|
case TAG_F64:
|
|
Out.type = scenery_entry_type::number;
|
|
Out.number = read_f64le( m_cursor, m_end );
|
|
break;
|
|
case TAG_QTOKEN:
|
|
Out.type = scenery_entry_type::qtoken;
|
|
Out.text = resolve( head >> TAG_BITS );
|
|
break;
|
|
case TAG_TOKEN:
|
|
default:
|
|
Out.type = scenery_entry_type::token;
|
|
Out.text = resolve( head >> TAG_BITS );
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
namespace {
|
|
|
|
// minimal bounded thread pool for offloading twin serialization/writing
|
|
class bake_pool {
|
|
public:
|
|
bake_pool() {
|
|
unsigned const workers = std::max( 2u, std::min( 8u, std::thread::hardware_concurrency() ) );
|
|
for( unsigned i = 0; i < workers; ++i ) {
|
|
m_threads.emplace_back( [ this ] { run(); } );
|
|
}
|
|
}
|
|
~bake_pool() {
|
|
{ std::unique_lock<std::mutex> lock( m_mutex ); m_stop = true; }
|
|
m_wake.notify_all();
|
|
for( auto &thread : m_threads ) { if( thread.joinable() ) { thread.join(); } }
|
|
}
|
|
void enqueue( std::function<void()> Task ) {
|
|
{ std::unique_lock<std::mutex> lock( m_mutex ); m_tasks.emplace( std::move( Task ) ); ++m_pending; }
|
|
m_wake.notify_one();
|
|
}
|
|
void wait_idle() {
|
|
std::unique_lock<std::mutex> lock( m_mutex );
|
|
m_idle.wait( lock, [ this ] { return m_pending == 0; } );
|
|
}
|
|
private:
|
|
void run() {
|
|
for( ;; ) {
|
|
std::function<void()> task;
|
|
{
|
|
std::unique_lock<std::mutex> lock( m_mutex );
|
|
m_wake.wait( lock, [ this ] { return m_stop || ( false == m_tasks.empty() ); } );
|
|
if( m_stop && m_tasks.empty() ) { return; }
|
|
task = std::move( m_tasks.front() );
|
|
m_tasks.pop();
|
|
}
|
|
task();
|
|
{
|
|
std::unique_lock<std::mutex> lock( m_mutex );
|
|
if( --m_pending == 0 ) { m_idle.notify_all(); }
|
|
}
|
|
}
|
|
}
|
|
std::vector<std::thread> m_threads;
|
|
std::queue<std::function<void()>> m_tasks;
|
|
std::mutex m_mutex;
|
|
std::condition_variable m_wake;
|
|
std::condition_variable m_idle;
|
|
bool m_stop { false };
|
|
std::size_t m_pending { 0 };
|
|
};
|
|
|
|
bake_pool &pool() {
|
|
static bake_pool instance;
|
|
return instance;
|
|
}
|
|
|
|
// results recorded by worker threads, drained and logged on the main thread
|
|
std::mutex g_resultmutex;
|
|
std::vector<std::string> g_resultlog; // success lines, in completion order
|
|
std::vector<std::string> g_resultfail; // failure paths
|
|
|
|
} // anonymous namespace
|
|
|
|
void
|
|
scenerybinary_write_async( std::unique_ptr<scenery_binary_writer> Writer, std::string Path, scenery_file_kind Kind ) {
|
|
// std::function requires a copyable target, so move the writer into a shared_ptr
|
|
std::shared_ptr<scenery_binary_writer> writer { std::move( Writer ) };
|
|
pool().enqueue( [ writer, path = std::move( Path ), Kind ] {
|
|
std::ofstream output( path, std::ios::binary );
|
|
bool const ok = output.good() && writer->write( output, Kind );
|
|
output.flush();
|
|
std::lock_guard<std::mutex> lock( g_resultmutex );
|
|
if( ok ) {
|
|
g_resultlog.emplace_back( "Compiled binary scenery: " + path + " (" + std::to_string( writer->entry_count() ) + " entries)" );
|
|
}
|
|
else {
|
|
g_resultfail.emplace_back( path );
|
|
}
|
|
} );
|
|
}
|
|
|
|
void
|
|
scenerybinary_wait_all() {
|
|
pool().wait_idle();
|
|
std::lock_guard<std::mutex> lock( g_resultmutex );
|
|
for( auto const &line : g_resultlog ) { WriteLog( line ); }
|
|
for( auto const &path : g_resultfail ) { ErrorLog( "Failed to write binary scenery \"" + path + "\"" ); }
|
|
g_resultlog.clear();
|
|
g_resultfail.clear();
|
|
}
|
|
|
|
std::string
|
|
scenerybinary_extension_for( std::string const &Sourcefile ) {
|
|
|
|
auto const lower { ToLower( Sourcefile ) };
|
|
if( lower.size() >= 4 && lower.compare( lower.size() - 4, 4, ".inc" ) == 0 ) {
|
|
return SCENERYBINARY_EXT_INC;
|
|
}
|
|
if( lower.size() >= 4 && lower.compare( lower.size() - 4, 4, ".scm" ) == 0 ) {
|
|
return SCENERYBINARY_EXT_SCM;
|
|
}
|
|
if( lower.size() >= 4 && lower.compare( lower.size() - 4, 4, ".ctr" ) == 0 ) {
|
|
return SCENERYBINARY_EXT_CTR;
|
|
}
|
|
// default: treat as a top-level scenario file (.scn)
|
|
return SCENERYBINARY_EXT_SCN;
|
|
}
|
|
|
|
} // namespace scene
|