Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,40 @@ jobs:
export PATH=$(echo $PATH | tr ':' '\n' | grep -v "Git/bin" | tr '\n' ':')
cmake -S . -B build -G "MinGW Makefiles" \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_BENCHMARKS=ON \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
cmake --build build -j 4

- name: Configure and Build (Linux/macOS)
if: runner.os != 'Windows'
run: |
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
-DBUILD_BENCHMARKS=ON \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
cmake --build build -j 4

- name: Run CTest
shell: bash
run: |
# Исправление бага: раньше CI только собирал проект. Inventory guard
# явно падает, если regression-target отсутствует, вместо тихого pass.
ctest --test-dir build -N | tee ctest_inventory.txt
if grep -q "Total Tests: 0" ctest_inventory.txt; then
echo "CTest registered zero tests"
exit 1
fi
ctest --test-dir build --output-on-failure

- name: Benchmark Smoke
shell: bash
run: |
# Исправление бага: benchmark-регрессии теперь ловятся inventory-check
# и одним коротким smoke-case вместо полного медленного набора.
if [ "$RUNNER_OS" = "Windows" ]; then
BENCHMARK_EXE="build/benchmarks.exe"
else
BENCHMARK_EXE="build/benchmarks"
fi
"$BENCHMARK_EXE" --benchmark_list_tests=true | tee benchmark_inventory.txt
grep -q "^SimulationFixture/Correct/125$" benchmark_inventory.txt
"$BENCHMARK_EXE" --benchmark_filter="^SimulationFixture/Correct/125$" --benchmark_min_time=0.001s --benchmark_repetitions=1
17 changes: 9 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,23 @@ jobs:
-DCMAKE_EXE_LINKER_FLAGS="-static -static-libgcc -static-libstdc++"

cmake --build build --config Release -j 4
# Исправление бага: упаковка release раньше вручную копировала выбранные
# файлы и могла пропустить wgpu_native.dll. Теперь ставим Runtime component.
cmake --install build --prefix release_dist --component Runtime

- name: Check Dependencies (Verification)
shell: bash
run: |
objdump -p LatticeLab.exe | grep "DLL Name"
# Исправление бага: проверяем именно те runtime-файлы, которые нужны zip.
test -f release_dist/LatticeLab.exe
test -f release_dist/wgpu_native.dll
objdump -p release_dist/LatticeLab.exe | grep "DLL Name"

- name: Prepare Release Artifacts
shell: bash
run: |
mkdir -p release_dist/assets
mkdir -p release_dist/demo
cp LatticeLab.exe release_dist/
cp -r assets/* release_dist/assets/
echo "Contents of demo:"
ls -R demo || true
cp -r demo/scenes release_dist/demo/
test -d release_dist/assets
test -d release_dist/demo/scenes
cd release_dist
7z a ../LatticeLab-${{ github.event.inputs.version }}-win64.zip *

Expand Down
23 changes: 18 additions & 5 deletions App/save_system/AppStateIO.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ namespace {
return std::string(value.substr(begin, end - begin));
}

bool hasConsistentAtomArrays(const SimulationSaveState& state) {
const size_t atomCount = state.x.size();
// Исправление бага: binary loads раньше доверяли atom arrays до
// AtomStorage::init. Inconsistent saves отклоняются до mutation state.
return state.atomMobileCount <= atomCount && state.y.size() == atomCount && state.z.size() == atomCount &&
state.vx.size() == atomCount && state.vy.size() == atomCount && state.vz.size() == atomCount &&
state.atomType.size() == atomCount && state.atomCharge.size() == atomCount;
}

std::string encodeBase64(std::span<const std::byte> data) {
static constexpr char kAlphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

Expand Down Expand Up @@ -408,15 +417,19 @@ void AppStateIO::loadBinary(Simulation& simulation, IRenderer& renderer, std::st
return;
}

// Заголовок
const auto& header = appState.header;
simulation.setWorldTitle(header.title);
simulation.setWorldDescription(header.description);

// Симуляция
const auto& simState = appState.simulation;
if (!hasConsistentAtomArrays(simState)) {
std::cerr << "Invalid atom arrays in save file" << std::endl;
return;
}

simulation.clear();
// Исправление бага: title/description раньше задавались до clear(), который
// сразу их стирал. Metadata назначается после очистки старой scene.
simulation.setWorldTitle(header.title);
simulation.setWorldDescription(header.description);

simulation.setSizeBox(simState.boxSize, simState.gridCellSize);
simulation.setNeighborListCutoff(simState.neighborListCutoff);
Expand All @@ -435,7 +448,7 @@ void AppStateIO::loadBinary(Simulation& simulation, IRenderer& renderer, std::st
const uint64_t atomCount = simState.x.size();

AtomStorage& atoms = simulation.atoms();
simulation.reserveAtoms(atoms.size());
simulation.reserveAtoms(atomCount);
atoms.init(atomCount, atomMobileCount, simState.x, simState.y, simState.z, simState.vx, simState.vy, simState.vz, simState.atomType,
simState.atomCharge);
simulation.finalizeAtomBatch();
Expand Down
6 changes: 4 additions & 2 deletions Benchmarks/BenchmarkScenes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ namespace Benchmarks {

switch (benchmarkCase.scene) {
case SceneKind::IdealCrystal3D:
buildCrystal2D(simulation, benchmarkCase);
break;
// Исправление бага: этот case раньше вызывал 2D builder, поэтому
// имя benchmark не соответствовало измеряемой сцене.
buildIdealCrystal3D(simulation, benchmarkCase);
break;
case SceneKind::Crystal2D:
buildCrystal2D(simulation, benchmarkCase);
break;
Expand Down
6 changes: 6 additions & 0 deletions Benchmarks/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,9 @@ target_link_libraries(benchmarks PRIVATE
benchmark::benchmark_main
latticelab_lib
)

# Исправление бага: benchmarks нужен тот же WebGPU runtime DLL рядом с executable,
# что и приложению; иначе renderer benchmark smoke может упасть вне корня исходников.
if(COMMAND target_copy_webgpu_binaries)
target_copy_webgpu_binaries(benchmarks)
endif()
17 changes: 17 additions & 0 deletions Benchmarks/fixtures/SimulationFixture.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <algorithm>
#include <cstdlib>
#include <memory>
#include <string>
Expand Down Expand Up @@ -65,6 +66,9 @@ class SimulationFixture : public benchmark::Fixture {
.world = simulation_->world(),
.forceField = simulation_->forceField(),
.neighborList = simulation_->neighborList(),
// Исправление бага: передаём simStep, чтобы stats rebuild NeighborList
// оставались meaningful после force-entry refresh.
.simStep = simulation_->getSimStep(),
.allowBondFormation = simulation_->isBondFormationEnabled(),
.accelDamping = accelDamping,
.dt = static_cast<float>(Benchmarks::kDt),
Expand All @@ -82,10 +86,23 @@ class SimulationFixture : public benchmark::Fixture {

void prepareNeighborList() { simulation_->neighborList().build(simulation_->atoms(), simulation_->world()); }

void clearForcesAndEnergy() {
// Исправление бага: force benchmarks раньше накапливали force/energy
// между iterations, измеряя corrupted state вместо одного clean force pass.
AtomStorage& atoms = simulation_->atoms();
std::fill_n(atoms.fxData(), atoms.size(), 0.0f);
std::fill_n(atoms.fyData(), atoms.size(), 0.0f);
std::fill_n(atoms.fzData(), atoms.size(), 0.0f);
std::fill_n(atoms.energyData(), atoms.size(), 0.0f);
}

void prepareForCorrect() {
prepareForPredict();
StepData stepData = makeStepData();
StepOps::predictAndSync(stepData, &VerletScheme::predict);
// Исправление бага: benchmark fixtures готовятся с тем же post-predict
// refresh NeighborList, который ожидают реальные force paths integrator.
StepOps::refreshNeighborListIfNeeded(stepData);
StepOps::computeForces(stepData);
}

Expand Down
6 changes: 6 additions & 0 deletions Benchmarks/physics/BM_ComputeForces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ BENCHMARK_DEFINE_F(SimulationFixture, ComputeForcesWithNeighborList)(benchmark::
StepData stepData = makeStepData();

for (auto _ : state) {
// Исправление бага: каждая итерация начинается с чистых accumulators,
// чтобы benchmark измерял один force pass вместо накопленного state.
clearForcesAndEnergy();
StepOps::computeForces(stepData);
benchmark::DoNotOptimize(simulation_->atoms().size());
benchmark::ClobberMemory();
Expand All @@ -22,6 +25,9 @@ BENCHMARK_DEFINE_F(SimulationFixture, ComputePairInteractionsWithNeighborList)(b
prepareNeighborList();

for (auto _ : state) {
// Исправление бага: pair-interaction-only timing нужны такие же чистые
// buffers force и energy, как full force benchmark.
clearForcesAndEnergy();
simulation_->forceField().computePairInteractions(simulation_->world());
benchmark::DoNotOptimize(simulation_->atoms().size());
benchmark::ClobberMemory();
Expand Down
7 changes: 5 additions & 2 deletions Benchmarks/physics/BM_NeighborNeedListRebuild.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
// соседи"}
BENCHMARK_DEFINE_F(SimulationFixture, NeighborListNeedRebuild)(benchmark::State& state) {
rebuildScene();
// Исправление бага: этот benchmark раньше измерял invalid fast path. Сначала
// строим valid list, чтобы timed loop измерял scan displacement.
prepareNeighborList();

for (auto _ : state) {
simulation_->neighborList().needsRebuild(simulation_->atoms());
benchmark::DoNotOptimize(simulation_->neighborList().pairStorageSize());
const bool needsRebuild = simulation_->neighborList().needsRebuild(simulation_->atoms());
benchmark::DoNotOptimize(needsRebuild);
benchmark::ClobberMemory();
}

Expand Down
26 changes: 22 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ option(OPTIMIZE_FOR_NATIVE "Enable native CPU optimizations outside Debug builds
option(BUILD_BENCHMARKS "Build benchmarks" OFF)
option(ENABLE_IPO "Enable link-time optimization for non-Debug builds when supported" ON)

include(CTest)

set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
# Исправление бага: runtime-exe раньше записывались в корень исходников.
# Теперь они остаются внутри дерева сборки, чтобы артефакты debug/release/bench
# не перезаписывали друг друга и не загрязняли checkout.
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
set(FETCHCONTENT_BASE_DIR "${CMAKE_BINARY_DIR}/_deps" CACHE PATH "FetchContent base directory" FORCE)

# CMake 4.x: allow legacy dependencies that declare very old minimum policies.
Expand Down Expand Up @@ -149,10 +154,23 @@ target_copy_webgpu_binaries(${APP_NAME})

include(windows_resources)

install(TARGETS ${APP_NAME} RUNTIME DESTINATION .)
install(DIRECTORY "${CMAKE_SOURCE_DIR}/assets" DESTINATION .)
# Исправление бага: упаковка release раньше вручную копировала файлы и могла
# пропустить wgpu_native.dll. Компонент Runtime теперь является единым источником
# для exe, WebGPU runtime DLL, assets и demo scenes.
install(TARGETS ${APP_NAME} RUNTIME DESTINATION . COMPONENT Runtime)
if(WEBGPU_RUNTIME_LIB)
install(FILES "${WEBGPU_RUNTIME_LIB}" DESTINATION . COMPONENT Runtime)
endif()
install(DIRECTORY "${CMAKE_SOURCE_DIR}/assets" DESTINATION . COMPONENT Runtime)
if(EXISTS "${CMAKE_SOURCE_DIR}/demo/scenes")
install(DIRECTORY "${CMAKE_SOURCE_DIR}/demo/scenes" DESTINATION demo)
install(DIRECTORY "${CMAKE_SOURCE_DIR}/demo/scenes" DESTINATION demo COMPONENT Runtime)
endif()

if(BUILD_TESTING)
# Исправление бага: раньше CTest регистрировал ноль тестов. Regression-
# проверки остаются в обычных сборках, чтобы CI/local presets ловили
# возврат уже исправленных багов.
add_subdirectory(Tests)
endif()

if(BUILD_BENCHMARKS)
Expand Down
4 changes: 3 additions & 1 deletion CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@
{
"name": "bench",
"configurePreset": "bench",
"description": "Исправление бага: preset bench собирает benchmarks и latticelab_tests, чтобы smoke-проверка бенчмарков не проходила при отсутствующих regression-тестах.",
"targets": [
"benchmarks"
"benchmarks",
"latticelab_tests"
],
"jobs": 8
}
Expand Down
3 changes: 3 additions & 0 deletions Engine/Consts.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Consts {
inline constexpr float Epsilon = 1e-6f;
// Исправление бага: LJ/Coulomb kernels clamp tiny pair distances до этого
// floor, чтобы near-overlap saves не создавали non-finite force spikes.
inline constexpr float MinPairDistanceSqr = 0.25f;
}

namespace Units {
Expand Down
51 changes: 48 additions & 3 deletions Engine/NeighborSearch/NeighborList.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

void NeighborList::setCutoff(float cutoff) {
cutoff_ = cutoff;
// Исправление бага: ForceField нужен реальный physical cutoff отдельно от
// cutoff + skin, чтобы skin shell влиял на performance, а не на physics.
cutoffSqr_ = cutoff_ * cutoff_;
listRadius_ = cutoff_ + skin_;
listRadiusSqr_ = listRadius_ * listRadius_;
valid_ = false;
Expand All @@ -27,6 +30,7 @@ void NeighborList::setSkin(float skin) {

void NeighborList::setParams(float cutoff, float skin) {
cutoff_ = cutoff;
cutoffSqr_ = cutoff_ * cutoff_;
skin_ = skin;
listRadius_ = cutoff_ + skin_;
listRadiusSqr_ = listRadius_ * listRadius_;
Expand Down Expand Up @@ -63,6 +67,8 @@ void NeighborList::build(const AtomStorage& atoms, World& box) {

const SpatialGrid& grid = box.getGrid();
const uint32_t atomCount = static_cast<uint32_t>(atoms.size());
const uint32_t mobileCount = static_cast<uint32_t>(atoms.mobileCount());
const bool hasFixedAtoms = mobileCount < atomCount;
const float* RESTRICT x = atoms.xData();
const float* RESTRICT y = atoms.yData();
const float* RESTRICT z = atoms.zData();
Expand All @@ -75,7 +81,14 @@ void NeighborList::build(const AtomStorage& atoms, World& box) {
const float yi = y[i];
const float zi = z[i];
// запись всех соседей атома в массив
writeAtomNeighbors(grid, x, y, z, i, xi, yi, zi, neighbors_);
// Исправление бага: fixed atoms находятся после mobile atoms, но ForceField
// обходит только mobile rows. Mixed mobile-fixed pairs хранятся здесь.
if (!hasFixedAtoms) {
writeAtomNeighbors(grid, x, y, z, i, xi, yi, zi, neighbors_);
}
else if (i < mobileCount) {
writeMixedMobileAtomNeighbors(grid, x, y, z, i, mobileCount, xi, yi, zi, neighbors_);
}
offsets_[i + 1] = neighbors_.size();
}

Expand All @@ -89,7 +102,9 @@ void NeighborList::build(const AtomStorage& atoms, World& box) {
bool NeighborList::needsRebuild(const AtomStorage& atoms) const {
const size_t n = atoms.mobileCount();

if (!valid_ || n != refPosX_.size()) {
// Исправление бага: atom add/remove меняет row offsets, даже если mobile ref
// arrays выглядят valid, поэтому total atom count входит в rebuild gate.
if (!valid_ || n != refPosX_.size() || atoms.size() != atomCount()) {
return true;
}

Expand All @@ -105,7 +120,7 @@ bool NeighborList::needsRebuild(const AtomStorage& atoms) const {
const float* RESTRICT refZ = refPosZ_.data();

int rebuild = false;
#pragma GCC ivdep
LATTICELAB_IVDEP
for (uint32_t i = 0; i < n; ++i) {
const float dx = x[i] - refX[i];
const float dy = y[i] - refY[i];
Expand Down Expand Up @@ -134,6 +149,36 @@ uint32_t NeighborList::memoryBytes() const {

void NeighborList::resetStats() { stats_.reset(); }

void NeighborList::writeMixedMobileAtomNeighbors(const SpatialGrid& grid, const float* x, const float* y, const float* z,
const uint32_t atomIndex, const uint32_t mobileCount, const float xi, const float yi,
const float zi, std::vector<uint32_t>& outNeighbors) const {
const auto& offsets27 = grid.neighborOffsets27();
const int center = grid.linearCellOfAtom(atomIndex);

for (int k = 0; k < 27; ++k) {
for (uint32_t neighborIndex : grid.atomsInCell(center + offsets27[k])) {
if (neighborIndex == atomIndex) {
continue;
}

// Исправление бага: mobile-mobile pairs сохраняют half-list ownership;
// каждая mobile-fixed pair принадлежит row мобильного атома.
const bool ownsMobilePair = neighborIndex < atomIndex;
const bool ownsFixedPair = neighborIndex >= mobileCount;
if (!ownsMobilePair && !ownsFixedPair) {
continue;
}

const float dx = x[neighborIndex] - xi;
const float dy = y[neighborIndex] - yi;
const float dz = z[neighborIndex] - zi;
if (dx * dx + dy * dy + dz * dz <= listRadiusSqr_) {
outNeighbors.emplace_back(neighborIndex);
}
}
}
}

void NeighborList::reserveListBuffers(const AtomStorage& atoms) {
const size_t prevCapacity = neighbors_.capacity();
neighbors_.clear();
Expand Down
Loading