From f2f152e5d35a896c5cd43046579e5d23038ea3b3 Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Mon, 28 Sep 2026 20:53:58 +0200 Subject: [PATCH 1/6] AI: don't cut the power for a brake already released below 0.4 bar When the AI takes power after a brake release, determine_consist_state() marks the consist not Ready if any vehicle's distributor reports the lap or application state (b_hld | b_on), whatever the speed, and the sanity check in control_handles() then puts the master controller back to zero. A distributor stays in the lap position until the brake pipe has fully recharged, so a few seconds after a release, with 0.2-0.3 bar left in a cylinder, one wagon of a train is enough to throw an EU07 or EP09 that has just reached the 2nd-5th position back to zero; it starts again moments later. The check in motion was meant to be lenient (brake force over 10 kN), but the distributor state was applied at any speed. In motion it now counts only when the cylinder holds more than 0.4 bar, the UIC figure already used a few lines below for a released brake. At a stand the check is unchanged. Measured in real time against master, two runs of each scenery (a 20 per mille descent, flat and a 10 per mille climb with 32 consists each, and a descent with 15 freight trains of other wagon types), with a 20 km/h restriction one coach long: throttle-downs to zero in motion caused by readiness drop from 13 to 1, 7 to 2, 0 to 0 and 13 to 6. Each of those left has a wagon over 0.4 bar that is applying or lapped, or one still releasing with more than 10 kN at walking pace. No coupler broke; signal overruns, speeds in the restriction and rolling back on the climb are the same as on master within run-to-run spread. --- vehicle/Driver.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 9a6de859..7e92c7c9 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -5983,9 +5983,12 @@ TController::determine_consist_state() { auto const bp { std::max( 0.0, vehicle->BrakePress - ( vehicle->SpeedCtrlUnit.Parking ? vehicle->MaxBrakePress[ 0 ] * vehicle->StopBrakeDecc : 0.0 ) ) }; if (Ready) { // bo jak coś nie odhamowane, to dalej nie ma co sprawdzać - if( TestFlagAny(vehicle->Hamulec->GetBrakeStatus(), b_hld | b_on) - || ( vehicle->Vel < 1.0 ? bp > 0.4 : // ensure the brakes are sufficiently released when starting to move - vehicle->Fb * 0.001 > 10.0 ) ) { // once in motion we can make a more lenient check + auto const isbraking { TestFlagAny( vehicle->Hamulec->GetBrakeStatus(), b_hld | b_on ) }; + if( vehicle->Vel < 1.0 ? + isbraking || bp > 0.4 : // ensure the brakes are sufficiently released when starting to move + // once in motion a brake down to 0.4 bar counts as released (UIC), even if its distributor + // is still in the lap position while the brake pipe recharges + ( isbraking && bp > 0.4 ) || vehicle->Fb * 0.001 > 10.0 ) { Ready = false; } // Ra: odluźnianie przeładowanych lokomotyw, ciągniętych na zimno - prowizorka... From b71ad90c93ba909ca11c77b9cfe3b590616cadb8 Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Sat, 3 Oct 2026 14:32:09 +0200 Subject: [PATCH 2/6] Declare isbraking in the init-statement of the if --- vehicle/Driver.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index a6223689..4b6d81ff 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -5901,8 +5901,8 @@ TController::determine_consist_state() { auto const bp { std::max( 0.0, vehicle->BrakePress - ( vehicle->SpeedCtrlUnit.Parking ? vehicle->MaxBrakePress[ 0 ] * vehicle->StopBrakeDecc : 0.0 ) ) }; if (Ready) { // bo jak coś nie odhamowane, to dalej nie ma co sprawdzać - auto const isbraking { TestFlagAny( vehicle->Hamulec->GetBrakeStatus(), b_hld | b_on ) }; - if( vehicle->Vel < 1.0 ? + if( auto const isbraking { TestFlagAny( vehicle->Hamulec->GetBrakeStatus(), b_hld | b_on ) }; + vehicle->Vel < 1.0 ? isbraking || bp > 0.4 : // ensure the brakes are sufficiently released when starting to move // once in motion a brake down to 0.4 bar counts as released (UIC), even if its distributor // is still in the lap position while the brake pipe recharges From 746399c771368300396517af34a62fd6356d4204 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 22:46:31 +0200 Subject: [PATCH 3/6] fix(ui): panels dragged out of the simulator window get no taskbar or alt+tab entries of their own and are owned by the main window on windows (always over it, minimized with it); ctrl+tab of imgui (window switcher, on even without keyboard navigation since 1.91) switched off, it took the key of the boost of the train --- application/uilayer.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/application/uilayer.cpp b/application/uilayer.cpp index 29bbb06c..2dd771bd 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editoruilayer.h" #include "imgui/imgui_impl_glfw.h" +#include "imgui/imgui_internal.h" GLFWwindow *ui_layer::m_window{nullptr}; GLFWwindow *ui_layer::m_keywindow{nullptr}; @@ -174,6 +175,10 @@ void create_viewport_window(ImGuiViewport *Viewport) imgui_create_window(Viewport); if (Viewport->PlatformHandle != nullptr) glfwSetKeyCallback(static_cast(Viewport->PlatformHandle), ui_layer::viewport_key_callback); +#ifdef _WIN32 + if (Viewport->PlatformHandleRaw != nullptr) + ::SetWindowLongPtr(static_cast(Viewport->PlatformHandleRaw), GWLP_HWNDPARENT, reinterpret_cast(ImGui::GetMainViewport()->PlatformHandleRaw)); +#endif } } // namespace @@ -283,6 +288,8 @@ bool ui_layer::init(GLFWwindow *Window) IMGUI_CHECKVERSION(); ImGui::CreateContext(); m_imguiio = &ImGui::GetIO(); + ImGui::GetCurrentContext()->ConfigNavWindowingKeyNext = ImGuiKey_None; + ImGui::GetCurrentContext()->ConfigNavWindowingKeyPrev = ImGuiKey_None; // imgui.ini is read with the first frame, which may be of another mode than the editor editor_ui::register_settings(); @@ -395,6 +402,7 @@ bool ui_layer::init(GLFWwindow *Window) if (Global.ui_viewports && viewportsupport) { m_imguiio->ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; + m_imguiio->ConfigViewportsNoTaskBarIcon = true; auto &platformio{ImGui::GetPlatformIO()}; imgui_create_window = platformio.Platform_CreateWindow; platformio.Platform_CreateWindow = create_viewport_window; From 71a19f32fb101e987917c6fe75ad04ee04d26245 Mon Sep 17 00:00:00 2001 From: pwarszawski Date: Mon, 12 Oct 2026 06:54:17 +0200 Subject: [PATCH 4/6] Compare the brake force with 10 kN directly --- vehicle/Driver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 4b6d81ff..2f40b2e1 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -5906,7 +5906,7 @@ TController::determine_consist_state() { isbraking || bp > 0.4 : // ensure the brakes are sufficiently released when starting to move // once in motion a brake down to 0.4 bar counts as released (UIC), even if its distributor // is still in the lap position while the brake pipe recharges - ( isbraking && bp > 0.4 ) || vehicle->Fb * 0.001 > 10.0 ) { + ( isbraking && bp > 0.4 ) || vehicle->Fb > 10000.0 ) { // 10 kN Ready = false; } // Ra: odluźnianie przeładowanych lokomotyw, ciągniętych na zimno - prowizorka... From be3af9a34d675377206f5dbfc057c12635ffa621 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Wed, 14 Oct 2026 20:15:45 +0200 Subject: [PATCH 5/6] Add dds BC7 algorithm --- model/Texture.cpp | 220 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 212 insertions(+), 8 deletions(-) diff --git a/model/Texture.cpp b/model/Texture.cpp index f5b57ebb..136846a3 100644 --- a/model/Texture.cpp +++ b/model/Texture.cpp @@ -30,6 +30,17 @@ http://mozilla.org/MPL/2.0/. #define EU07_DEFERRED_TEXTURE_UPLOAD +#ifndef FOURCC_DX10 +#define FOURCC_DX10 (MAKEFOURCC('D','X','1','0')) +#endif +// BC7, core since GL 4.2 (ARB_texture_compression_bptc), the bundled glad is generated for 3.3 +#ifndef GL_COMPRESSED_RGBA_BPTC_UNORM +#define GL_COMPRESSED_RGBA_BPTC_UNORM 0x8E8C +#endif +#ifndef GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM +#define GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM 0x8E8D +#endif + // texture data read by a worker thread. the texture itself is only touched by the main thread, // which takes the data over when the texture is created on the gl side struct texture_request { @@ -156,6 +167,142 @@ private: bool m_stop { false }; }; +// dxgi formats and alpha modes of the dds dx10 extended header +constexpr std::uint32_t dxgi_format_bc7_typeless { 97 }; +constexpr std::uint32_t dxgi_format_bc7_unorm { 98 }; +constexpr std::uint32_t dxgi_format_bc7_unorm_srgb { 99 }; +constexpr std::uint32_t dds_alpha_mode_unknown { 0 }; +constexpr std::uint32_t dds_alpha_mode_opaque { 3 }; + +// returns Count bits starting at bit Offset of a 128-bit BC7 block +std::uint32_t bc7_bits( unsigned char const *Block, int const Offset, int const Count ) { + std::uint32_t result { 0 }; + for( int bit = 0; bit < Count; ++bit ) { + auto const idx { Offset + bit }; + result |= ( ( Block[ idx >> 3 ] >> ( idx & 7 ) ) & 1u ) << bit; + } + return result; } + +// expands a BC7 endpoint of given precision to 8 bits +std::uint32_t bc7_expand( std::uint32_t const Value, int const Bits ) { + return ( Value << ( 8 - Bits ) ) | ( Value >> ( 2 * Bits - 8 ) ); } + +// checks whether any texel of a BC7 block can come out noticeably translucent. +// it looks at the endpoints the alpha is interpolated between, so a translucent block is never reported as opaque +bool bc7_block_has_alpha( unsigned char const *Block ) { + // encoders sharing p-bits between colour and alpha (modes 6 and 7) can leave opaque alpha at 254 or 251 + constexpr std::uint32_t opaque { 250 }; + if( Block[ 0 ] == 0 ) { + // reserved mode, decodes to transparent black + return true; + } + int mode { 0 }; + while( ( Block[ 0 ] & ( 1 << mode ) ) == 0 ) { + ++mode; + } + switch( mode ) { + case 4: + case 5: { + // single subset with separate alpha endpoints. rotation swaps alpha with one of the colour channels + auto const rotation { static_cast( bc7_bits( Block, mode + 1, 2 ) ) }; + auto const colourbits { mode == 4 ? 5 : 7 }; + auto const alphabits { mode == 4 ? 6 : 8 }; + auto const bits { rotation == 0 ? alphabits : colourbits }; + auto const start { rotation == 0 ? 8 + 6 * colourbits : 8 + 2 * colourbits * ( rotation - 1 ) }; + for( int endpoint = 0; endpoint < 2; ++endpoint ) { + if( bc7_expand( bc7_bits( Block, start + endpoint * bits, bits ), bits ) < opaque ) { + return true; + } + } + return false; + } + case 6: { + // 7-bit alpha endpoints, each extended with its own p-bit + for( int endpoint = 0; endpoint < 2; ++endpoint ) { + auto const alpha { ( bc7_bits( Block, 49 + endpoint * 7, 7 ) << 1 ) | bc7_bits( Block, 63 + endpoint, 1 ) }; + if( alpha < opaque ) { + return true; + } + } + return false; + } + case 7: { + // two subsets, 5-bit alpha endpoints extended with their p-bits to 6 bits + for( int endpoint = 0; endpoint < 4; ++endpoint ) { + auto const alpha { ( bc7_bits( Block, 74 + endpoint * 5, 5 ) << 1 ) | bc7_bits( Block, 94 + endpoint, 1 ) }; + if( bc7_expand( alpha, 6 ) < opaque ) { + return true; + } + } + return false; + } + default: { + // modes 0-3 don't store alpha + return false; + } + } } + +// picks data components of a BC7 texture. a BC7 file doesn't tell whether alpha is used the way DXT1/DXT5 do, +// so unless the dx10 header states the alpha mode, a small mipmap is checked for translucent texels. +// the mipmap is always read from the file, so the result doesn't depend on whether the texture data is streamed +GLint bc7_components( std::istream &File, std::streamoff const Start, std::size_t const Datasize, unsigned int Width, unsigned int Height, unsigned int const Mapcount, std::uint32_t const Alphamode ) { + + if( Alphamode == dds_alpha_mode_opaque ) { return GL_RGB; } + if( Alphamode != dds_alpha_mode_unknown ) { return GL_RGBA; } + + // up to 256x256, small enough to read quickly while translucent areas a few texels wide still show in it + auto const blocks = [ & ]() { return static_cast( ( Width + 3 ) / 4 ) * ( ( Height + 3 ) / 4 ); }; + std::size_t offset { 0 }; + for( unsigned int level = 1; ( level < Mapcount ) && ( std::max( Width, Height ) > 256 ); ++level ) { + offset += blocks() * 16; + Width = std::max( Width / 2, 1u ); + Height = std::max( Height / 2, 1u ); + } + std::vector probe( blocks() * 16 ); + if( offset + probe.size() > Datasize ) { + // malformed file, err on the side of translucency like DXT5 does + return GL_RGBA; + } + auto const position { File.tellg() }; + File.seekg( Start + static_cast( offset ) ); + File.read( reinterpret_cast( probe.data() ), probe.size() ); + auto const readok { File.good() }; + File.clear(); + File.seekg( position ); + if( false == readok ) { + return GL_RGBA; + } + for( std::size_t block = 0; block < probe.size(); block += 16 ) { + if( bc7_block_has_alpha( &probe[ block ] ) ) { + return GL_RGBA; + } + } + return GL_RGB; } + +// BC7 is core since GL 4.2, older contexts and GLES need the extension. requires current gl context +bool bptc_supported() { + static bool const supported = []() { + if( ( false == Global.gfx_usegles ) + && ( ( GLVersion.major > 4 ) || ( ( GLVersion.major == 4 ) && ( GLVersion.minor >= 2 ) ) ) ) { + return true; + } + GLint count { 0 }; + ::glGetIntegerv( GL_NUM_EXTENSIONS, &count ); + for( GLint idx = 0; idx < count; ++idx ) { + auto const *extension { reinterpret_cast( ::glGetStringi( GL_EXTENSIONS, idx ) ) }; + if( extension == nullptr ) { + continue; + } + std::string_view const name { extension }; + if( ( name == "GL_ARB_texture_compression_bptc" ) + || ( name == "GL_EXT_texture_compression_bptc" ) ) { + return true; + } + } + return false; + }(); + return supported; } + } // namespace std::array opengl_texture::units = { 0 }; @@ -171,6 +318,8 @@ std::unordered_map opengl_texture::precompressed_formats = { GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, 8 }, { GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, 16 }, { GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, 16 }, + { GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM, 16 }, + { GL_COMPRESSED_RGBA_BPTC_UNORM, 16 }, }; std::unordered_map opengl_texture::drivercompressed_formats = @@ -210,6 +359,12 @@ std::unordered_map> opengl_texture::mapp { GL_RGB, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT }, { GL_RG, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT }, { GL_RED, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT } } }, + { GL_COMPRESSED_RGBA_BPTC_UNORM, { { GL_SRGB_ALPHA, GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM }, + { GL_SRGB, GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM }, + { GL_RGBA, GL_COMPRESSED_RGBA_BPTC_UNORM }, + { GL_RGB, GL_COMPRESSED_RGBA_BPTC_UNORM }, + { GL_RG, GL_COMPRESSED_RGBA_BPTC_UNORM }, + { GL_RED, GL_COMPRESSED_RGBA_BPTC_UNORM } } }, { GL_RGBA, { { GL_SRGB_ALPHA, GL_SRGB8_ALPHA8 }, { GL_SRGB, GL_SRGB8 }, { GL_RGBA, GL_RGBA8 }, @@ -651,9 +806,26 @@ opengl_texture::load_DDS() { DDSURFACEDESC2 ddsd = deserialize_ddsd(file); filesize -= 124; + // formats without a legacy fourcc code (BC7) are described by the dx10 extended header + std::uint32_t dxgiformat { 0 }; + std::uint32_t alphamode { dds_alpha_mode_unknown }; + if( ddsd.ddpfPixelFormat.dwFourCC == FOURCC_DX10 ) { + dxgiformat = sn_utils::ld_uint32( file ); + sn_utils::ld_uint32( file ); // resource dimension + sn_utils::ld_uint32( file ); // misc flags + auto const arraysize { sn_utils::ld_uint32( file ) }; + alphamode = sn_utils::ld_uint32( file ) & 0x7; // misc flags 2, the lowest bits hold the alpha mode + filesize -= 20; + if( arraysize > 1 ) { + WriteLog( "Bad texture: texture arrays aren't supported, in file \"" + name + "\"", logtype::texture ); + data_state = resource_state::failed; + return; + } + } + // - // This .dds loader supports the loading of compressed formats DXT1, DXT3 - // and DXT5. + // This .dds loader supports the loading of compressed formats DXT1, DXT3, + // DXT5 and BC7. // switch (ddsd.ddpfPixelFormat.dwFourCC) @@ -673,6 +845,17 @@ opengl_texture::load_DDS() { data_format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; break; + case FOURCC_DX10: + if( ( dxgiformat != dxgi_format_bc7_typeless ) + && ( dxgiformat != dxgi_format_bc7_unorm ) + && ( dxgiformat != dxgi_format_bc7_unorm_srgb ) ) { + data_state = resource_state::failed; + return; + } + // BC7's compression ratio is 4:1. srgb is picked by the material, same as for DXTn + data_format = GL_COMPRESSED_RGBA_BPTC_UNORM; + break; + default: data_state = resource_state::failed; return; @@ -709,13 +892,26 @@ opengl_texture::load_DDS() { data_state = resource_state::failed; return; } + // dds data with top-left origin is flipped block by block. BC7 blocks can't be flipped that way, + // BC7 textures have to be stored with bottom-left origin regardless of the setting + bool const flip { Global.dds_upper_origin && ( data_format != GL_COMPRESSED_RGBA_BPTC_UNORM ) }; + if( Global.dds_upper_origin && ( false == flip ) ) { + WriteLog( "Warning: BC7 texture \"" + name + "\" can't be flipped for ddsupperorigin, it's loaded as stored", logtype::texture ); + } + GLint const components { ( + data_format == GL_COMPRESSED_RGBA_BPTC_UNORM ? + bc7_components( file, static_cast( file.tellg() ) + offset, datasize, data_width, data_height, data_mapcount, alphamode ) : + ddsd.ddpfPixelFormat.dwFourCC == FOURCC_DXT1 ? + GL_RGB : + GL_RGBA ) }; + if( true == Global.gfx_texture_streaming ) { // the data is read by a worker thread, everything else about the texture is known already request = std::make_shared(); request->file = name + type; request->offset = static_cast( file.tellg() ) + offset; request->size = datasize; - request->flip = Global.dds_upper_origin; + request->flip = flip; request->fourcc = ddsd.ddpfPixelFormat.dwFourCC; request->width = data_width; request->height = data_height; @@ -723,8 +919,7 @@ opengl_texture::load_DDS() { request->blocksize = blockSize; texture_loader::instance().submit( request ); - data_components = - ddsd.ddpfPixelFormat.dwFourCC == FOURCC_DXT1 ? GL_RGB : GL_RGBA; + data_components = components; data_state = resource_state::loading; @@ -744,7 +939,7 @@ opengl_texture::load_DDS() { // we're storing texture data internally with bottom-left origin, // while DDS stores it with top-left origin. we need to flip it. - if (Global.dds_upper_origin) + if (flip) { char *mipmap = (char*)&data[0]; int mapcount = data_mapcount, @@ -766,8 +961,7 @@ opengl_texture::load_DDS() { } } - data_components = - ddsd.ddpfPixelFormat.dwFourCC == FOURCC_DXT1 ? GL_RGB : GL_RGBA; + data_components = components; data_state = resource_state::good; @@ -1099,6 +1293,16 @@ opengl_texture::create( bool const Static ) { return false; } + if( ( data_format == GL_COMPRESSED_RGBA_BPTC_UNORM ) + && ( false == bptc_supported() ) ) { + ErrorLog( "Bad texture: \"" + name + "\" uses BC7 compression, which the graphics driver doesn't support", logtype::texture ); + data = std::vector(); + data_state = resource_state::failed; + // NOTE: temporary workaround for texture assignment errors, same as in load() + id = 0; + return false; + } + // TODO: consider creating and storing low-res version of the texture if it's ever unloaded from the gfx card, // as a placeholder until it can be loaded again if( id == -1 ) { From 04af7d57581cea610f89ed392df68931a619e0e1 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 21:11:38 +0200 Subject: [PATCH 6/6] fix(python): stop python workers before destroying windows on exit --- application/application.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/application.cpp b/application/application.cpp index 8afd50db..119cb021 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -701,11 +701,12 @@ void eu07_application::exit() ui_layer::shutdown(); + // the python workers detach their GL context on the way out, so they have to be done before the windows they use are destroyed + m_taskqueue.exit(); for (auto *window : m_windows) { glfwDestroyWindow(window); } - m_taskqueue.exit(); glfwPollEvents(); // TODO: This fixes a segfault on Wayland when closing. Remove after updating glfw to 3.5. glfwTerminate();