Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trying to fall asleep next to a tree is incredibly slow, but waiting is not #73552

Closed
NetSysFire opened this issue May 6, 2024 · 50 comments · Fixed by #73590
Closed

Trying to fall asleep next to a tree is incredibly slow, but waiting is not #73552

NetSysFire opened this issue May 6, 2024 · 50 comments · Fixed by #73590
Labels
Code: Performance Performance boosting code (CPU, memory, etc.) Fields / Furniture / Terrain / Traps Objects that are part of the map or its features. (S2 - Confirmed) Bug that's been confirmed to exist

Comments

@NetSysFire
Copy link
Member

NetSysFire commented May 6, 2024

Describe the bug

There are no nearby monsters either.

I ran the game in gdb to see what it was doing:

Thread 1 (Thread 0x7ffff748f7c0 (LWP 1664080) "cataclysm-tiles"):
#0  0x0000555555e8816c in trig_dist () at src/line.h:159
No locals.
#1  rl_dist () at src/line.h:184
No locals.
#2  0x0000555556b324d3 in cast_horizontal_zlight_segment<1, 0, 0, 1, 1, float, &(sight_calc(float const&, float const&, int const&)), &(sight_check(float const&, float const&)), &(accumulate_transparency(float const&, float const&, int const&))> () at src/shadowcasting.cpp:298
No locals.
#3  0x0000555556b371ac in cast_zlight<float, &(sight_calc(float const&, float const&, int const&)), &(sight_check(float const&, float const&)), &(accumulate_transparency(float const&, float const&, int const&))> () at src/shadowcasting.cpp:607
No locals.
#4  0x000055555655ef65 in map::build_seen_cache () at src/lightmap.cpp:1019
No locals.
#5  0x00005555565d5b27 in map::build_map_cache () at src/map.cpp:9591
No locals.
#6  0x00005555561ca439 in do_turn () at src/do_turn.cpp:655
No locals.
#7  0x0000555555ccb9df in main () at src/main.cpp:868

Weird. I tried sleeping outside of the improvised shelter but no dice. Wild guess: treetops? I know these would do z-level shading things. And indeed going just a bit west into the open makes this issue go away.

Attach save file

Innawoods-trimmed.tar.gz

Reproducible by trying to sleep adjacent to a tree.

Steps to reproduce

(While the save is obviously an innawoods one, I highly doubt this is mod specific)

  1. Load save.
  2. Try to sleep.
  3. The progress is excruciating slow.
  4. Cancel and see if waiting is faster.
  5. Waiting is indeed blazingly fast.
  6. Go a bit west, away from trees.
  7. Try sleeping again on the floor.
  8. It is fast.

Expected behavior

Falling asleep progresses as fast as waiting under tree tops.

CC @PatrikLundell since you did a lot of tree tops related things and might know whats going on here.

Screenshots

No response

Versions and configuration

  • OS: Linux
    • OS Version: LSB Version: n/a; Distributor ID: Arch; Description: Arch Linux; Release: rolling; Codename: n/a;
  • Game Version: cdda-experimental-2024-05-06-1236 3428b61 [64-bit]
  • Graphics Version: Tiles
  • Game Language: System language []
  • Mods loaded: [
    Dark Days Ahead [dda],
    Disable NPC Needs [no_npc_food],
    Portal Storms Ignore NPCs [personal_portal_storms],
    Slowdown Fungal Growth [no_fungal_growth],
    Innawood [innawood]
    ]

Additional context

No response

@NetSysFire NetSysFire added Code: Performance Performance boosting code (CPU, memory, etc.) (S1 - Need confirmation) Report waiting on confirmation of reproducibility Z-levels Levels below and above ground. labels May 6, 2024
@PatrikLundell
Copy link
Contributor

Sorry, I have no idea what might be going on. The only thing I've done is to try to make the buggers show up when they're supposed to (and disappear when the rest of the tree does), but I have no idea what effects they may have beyond that.

If it is caused by treetops then rebug removing them with map editing should make a difference. I'd also try to wait first and sleep afterwards (to catch the case when whatever is causing it goes away with time). I might also try adjusting the time to night time with a heavy cloud cover to make sure there is no light to shadow (although that doesn't guarantee the processing is skipped).

I'd also try to teleport away to sleep (again to catch the case where something with the character passes with time).

@NetSysFire
Copy link
Member Author

  • Making the tree top t_open_air changes nothing.
  • In fact, teleporting onto a field (where sleeping is fast), and manually spawning a tree and sleeping next to it is sufficient to cause the slowdown.
  • While doing that, setting the weather to cloudy or rain storm does not change things. I also made my character blind using a blindfold and removed the clairvoyance and nightvision debug mutations.
  • Teleporting into the ground, z-level -1, making the tile you are on t_dirt and a tile next to you a tree does not trigger this bug.
  • However: Teleporting onto a radio tower, sleeping is still fast there, and making a tile next to you a tree does trigger the slowness.
  • Thus I think for some reason only z >= 0 causes the issue.

@NetSysFire NetSysFire changed the title Trying to fall asleep under tree tops is incredibly slow, but waiting is not Trying to fall asleep next to a tree is incredibly slow, but waiting is not May 6, 2024
@PatrikLundell
Copy link
Contributor

Did you spawn the treetop as well, or only the base of it? I suspect the treetop doesn't spawn unless you causes the game to load submaps.

@NetSysFire
Copy link
Member Author

It makes no difference whether or not a tree top is present.

@PatrikLundell
Copy link
Contributor

I tried to hack trees to not specify any roofs, but it made no difference (in case there's some weird extra "should there be a roof here" stuff). However, I've encountered a new problem, namely that the game crashes (with the hack reverted), presumably during wildlife movement:
Exception thrown at 0x00007FF70CFA3D99 in cataclysm-tiles.exe: 0xC0000005: Access violation reading location 0xFFFFFFFFFFFFFFFF.

cataclysm-tiles.exe!monster::move() Line 1164 C++
cataclysm-tiles.exe!`anonymous namespace'::monmove() Line 316 C++
cataclysm-tiles.exe!do_turn() Line 657 C++
cataclysm-tiles.exe!WinMain(HINSTANCE__ * formal, HINSTANCE * __formal, char * __formal, int __formal) Line 868 C++

on line
if( !here.inbounds( candidate ) ) {
in monmove.cpp (line 1164 in the master downloaded a short while ago), but I can't figure out what the erroneous pointer is supposed to be. Destination is (106, 25, 0), and so within the bubble, and distance_to_target is 2.0, so pos() should also be in range, as should all candidate positions fed to inbounds. here looks like a normal map to me.

@PatrikLundell
Copy link
Contributor

/confirmed

I can see what you're reporting, but have no idea what's causing it.
I've recompiled in debug mode, causing everything to be even more glacial, but fail to repeat and catch the crash.

@github-actions github-actions bot added (S2 - Confirmed) Bug that's been confirmed to exist and removed (S1 - Need confirmation) Report waiting on confirmation of reproducibility labels May 7, 2024
@IdleSol
Copy link
Contributor

IdleSol commented May 7, 2024

A few fun facts. To test it out, I found a lonely standing tree.

  1. Slowdown occurs on the tiles adjacent to the tree. They are marked with a target in the picture.

test 1

  1. On the other tiles, there is no slowdown (or not noticeable).

Note. Character position for further testing

test 2

  1. I saved and loaded the slowdown remains.
  2. I turned off the game and restarted. The slowdown remains.
  3. I cut down a tree. The slowdown remains.

test 3

  1. But the area where the slowdown manifests itself has changed.

test 4
Unmarked, but on the trunk and stump, the slowdown is there too.

  1. I repeated the steps with save and load, to rule out some variations. The slowdown remains in the same tiles.

  2. I started chopping the trunk and stump, into logs. The slowdown was disappearing from the neighboring tiles.

test 5
test 6

  1. When I chopped the last part into logs, the slowdown disappeared.

  2. To test it, using the debug menu, I created a new tree trunk on the ground. The slowdown appeared.

  3. Modified the file by removing all flags. This has no effect whatsoever.

No other ideas. Except for the idea of hooking someone up to see what process is causing the game to slow down.

@IdleSol
Copy link
Contributor

IdleSol commented May 7, 2024

I found the parameter that's causing the slowdown. It's all about coverage. On a tree trunk, it's 45.

Change it to 0 and compare the speed when you try to sleep.

At 10, there's already a noticeable slowdown. 100 and 45, in my opinion, are no different. I haven't really measured it accurately.

So I think the reason for this is the coverage parameter. More precisely, in the interaction between this parameter and the function responsible for sleep. But we need someone who can check this and tell us more precisely.

UPD.

Some changes. If you try to fall asleep on a tree trunk, there is no slowdown. Provided there are no other items with coverage greater than 0 nearby.

If you teleport into a tree, there will be slowdown. But because the game forcibly moves you to a neighboring square. Surround the tree with walls and you can sleep without slowdown.

Surround the tree with walls and leave one free tile. And you can sleep on it without slowdown.
test 7

Or place a tree trunk in the house. Similar. There is no slowdown.

The bed has a 40% cover. But it has no effect on speed. As long as you don't take the bed outside.

@NetSysFire NetSysFire added Fields / Furniture / Terrain / Traps Objects that are part of the map or its features. and removed Z-levels Levels below and above ground. labels May 7, 2024
@worm-girl
Copy link
Contributor

worm-girl commented May 7, 2024

Cover has some functionality in lightmap.cpp and it's possible that the issue is there. Building the vision cache is really resource intensive.

@IdleSol
Copy link
Contributor

IdleSol commented May 7, 2024

And you probably answered correctly.

bool map::build_vision_transparency_cache( const int zlev )

I was paying attention to that condition:

} else if( ( is_crouching || is_prone || low_profile ) && coverage( loc ) >= 30 ) {

So I went to a tree, sat down and waited for 5 minutes. Which resulted in a similar slowdown of the game. If the character is standing, this condition is ignored.

@RenechCDDA
Copy link
Member

Well this is weird. The function name implies we're always rebuilding the cache, but we're... only sometimes?

In any case, this SHOULD be perfectly safe. And I can confirm that it eliminates this performance issue entirely:

index bbe88a5b40..f73b3fc18d 100644
--- a/src/lightmap.cpp
+++ b/src/lightmap.cpp
@@ -219,8 +219,10 @@ bool map::build_vision_transparency_cache( const int zlev )
             vision_transparency_cache[p.x][p.y] = LIGHT_TRANSPARENCY_OPEN_AIR;
         } else if( ( is_crouching || is_prone || low_profile ) && coverage( loc ) >= 30 ) {
             // If we're crouching or prone behind an obstacle, we can't see past it.
-            vision_transparency_cache[loc.x][loc.y] = LIGHT_TRANSPARENCY_SOLID;
-            dirty = true;
+            if( vision_transparency_cache[loc.x][loc.y] != LIGHT_TRANSPARENCY_SOLID ) {
+                vision_transparency_cache[loc.x][loc.y] = LIGHT_TRANSPARENCY_SOLID;
+                dirty = true;
+            }
         }
     }
 

@IdleSol
Copy link
Contributor

IdleSol commented May 8, 2024

Is the problem exactly in this function? Maybe I misunderstood something.

As I understand it:

  1. The character's position is taken
  2. LIGHT_TRANSPARENCY_OPEN_AIR is set for the tile the character is standing on
  3. For tiles in radius 1, the transparency cache is updated and LIGHT_TRANSPARENCY_SOLID is set if the conditions are met.

In the tree example, only for one tile the vision_transparency_cache parameter is updated. And it doesn't explain why it doesn't happen in a house. Or if you surround the tree with walls.

Perhaps we should look at a function that uses this value?

@RenechCDDA
Copy link
Member

It sets the seen_cache as dirty every turn:

Cataclysm-DDA/src/map.cpp

Lines 9560 to 9562 in 6a8d75a

for( int z = minz; z <= maxz; z++ ) {
seen_cache_dirty |= build_vision_transparency_cache( z );
}

Which then triggers the rebuild of the seen cache. which is the entirety of the hot path during profiling:

Cataclysm-DDA/src/map.cpp

Lines 9580 to 9582 in 6a8d75a

if( seen_cache_dirty ) {
if( inbounds( p ) ) {
build_seen_cache( getlocal( p ), zlev, sr );

I don't see any reason to set the cache as dirty if the cache does not change.

Notably the seen cache is already being properly dirtied if the player moved or their vision range changed.

seen_cache_dirty |= player_prev_pos != p || sr != player_prev_range || camera_cache_dirty;

So the code I'm suggesting changing is explicitly resetting the cache every turn even when the situation doesn't change. Actually, we can be even safer here by rebuilding the cache if movemode changes

index bbe88a5b40..7e5d283ff6 100644
--- a/src/lightmap.cpp
+++ b/src/lightmap.cpp
@@ -212,6 +212,7 @@ bool map::build_vision_transparency_cache( const int zlev )
     bool low_profile = player_character.has_effect( effect_quadruped_full ) &&
                        player_character.is_running();
     bool is_prone = player_character.is_prone();
+    static move_mode_id previous_move_mode = player_character.current_movement_mode();
 
     for( const tripoint &loc : points_in_radius( p, 1 ) ) {
         if( loc == p ) {
@@ -219,8 +220,11 @@ bool map::build_vision_transparency_cache( const int zlev )
             vision_transparency_cache[p.x][p.y] = LIGHT_TRANSPARENCY_OPEN_AIR;
         } else if( ( is_crouching || is_prone || low_profile ) && coverage( loc ) >= 30 ) {
             // If we're crouching or prone behind an obstacle, we can't see past it.
-            vision_transparency_cache[loc.x][loc.y] = LIGHT_TRANSPARENCY_SOLID;
-            dirty = true;
+            if( vision_transparency_cache[loc.x][loc.y] != LIGHT_TRANSPARENCY_SOLID ||
+                previous_move_mode != player_character.current_movement_mode() ) {
+                vision_transparency_cache[loc.x][loc.y] = LIGHT_TRANSPARENCY_SOLID;
+                dirty = true;
+            }
         }
     }
 

@GuardianDll
Copy link
Member

i am still not very familiar to what happens here, but i tested the last suggestion, and what i spot is that it doesn't help to solve the bug in any way, comparing to the first diff
image

@Brambor
Copy link
Contributor

Brambor commented May 8, 2024

What about crafting? When a flashlight (the only source of light in a dark room) runs out of batteries the character should no longer be able to see to continue crafting. Will the character stop crafting with these changes?

@IdleSol
Copy link
Contributor

IdleSol commented May 9, 2024

bool map::build_vision_transparency_cache( const int zlev )
...
    static move_mode_id previous_move_mode = player_character.current_movement_mode();

    for( const tripoint &loc : points_in_radius( p, 1 ) ) {
        if( loc == p ) { 
            vision_transparency_cache[p.x][p.y] = LIGHT_TRANSPARENCY_OPEN_AIR;
        }
        else if( ( is_crouching || is_prone || low_profile ) && coverage( loc ) >= 30 ) {
            if( vision_transparency_cache[loc.x][loc.y] != LIGHT_TRANSPARENCY_SOLID ||
                previous_move_mode != player_character.current_movement_mode() ) {
                    vision_transparency_cache[loc.x][loc.y] = LIGHT_TRANSPARENCY_SOLID;
                    dirty = true;
            }
        }
    }
    return dirty;
}

I'm sure I'm misunderstanding something. But doesn't it look like the following?

Some_function
...
    X = A

    for i=1 to 9 do
        if i = 5 then  
            vision_transparency_cache[p.x][p.y] = LIGHT_TRANSPARENCY_OPEN_AIR;
        else if ( Some_condition ) then 
            if X != A then 
                vision_transparency_cache[loc.x][loc.y] = LIGHT_TRANSPARENCY_SOLID;
                dirty = true;
            end
        end
    end
    return dirty;
end

I'm confused about a few things, but the main one is:

    X = A
    if X != A then 

This condition is always false.

Other things:

  1. Is the cycle definitely doing 9 turns? It doesn't go to infinity, does it?
  2. Do we need to check the loc == p condition at each cycle turn? Can it be moved outside the cycle?
  3. Do we need to check the previous_move_mode != player_character.current_movement_mode() condition at every cycle turn?

As far as I understand, the character cannot change its movement mode during the execution of this function.

@RenechCDDA
Copy link
Member

RenechCDDA commented May 9, 2024

I'm confused about a few things, but the main one is:

https://en.cppreference.com/w/cpp/language/static

Basically the value held inside the static variable is retained for next time the function is run.

Is the cycle definitely doing 9 turns? It doesn't go to infinity, does it?

The specific for loop I suggested modifying only does the 9 squares around the player's XY coordinates, on the z-level it was called for. However the call to map::build_vision_transparency_cache calls it across all z-levels, sequentially. It only checks valid z-levels.

Do we need to check the previous_move_mode != player_character.current_movement_mode() condition at every cycle turn?

C++ operator precedence means that it can return early. It is an extraordinarily cheap check compared to rebuilding the entire vision cache, so regardless... yeah.

@RenechCDDA
Copy link
Member

Basically the value held inside the static variable is retained for next time the function is run.

Oh and that's why it doesn't work in Guardian's testing lol, it needs another assignment or else it's stuck on the very first value it picks up forever.

@IdleSol
Copy link
Contributor

IdleSol commented May 9, 2024

I still continue to not understand.

Cataclysm-DDA/src/map.cpp

Lines 9560 to 9562 in 6a8d75a

for( int z = minz; z <= maxz; z++ ) {
seen_cache_dirty |= build_vision_transparency_cache( z );
}

Some_function
    for i=1 to 21 do
        Some_cache_access_operations
        if i != p.z then  
            return false
        else
            for i=1 to 9 do
                if i = 5 then  
                    vision_transparency_cache[p.x][p.y] = LIGHT_TRANSPARENCY_OPEN_AIR;
                else if ( Some_condition ) then 
                    if X != A then 
                        vision_transparency_cache[loc.x][loc.y] = LIGHT_TRANSPARENCY_SOLID;
                        dirty = true;
                    end
                end
            end
            return dirty;
        end
    end
end

@GuardianDll
Copy link
Member

GuardianDll commented May 9, 2024

should #73590 be modified, to use the second suggestion (if someone update it to contain assignment?), or the first one is okay? found the comment in 73590, will do

@worm-girl
Copy link
Contributor

worm-girl commented May 15, 2024

I had help from @andrei8l when I messed with vision, perhaps they can weigh in (sorry for the ping if not!), as they seem to know their stuff.

@SurFlurer
Copy link
Contributor

vision_transparency_cache and transparency_cache can be different but still be in sync. That memory copying overhead can be eliminated by updating vision_transparency_cache only when transparency_cache is dirty, player's movement mode changes, or the map coverage changes ( I believe that ter/furn/veh changes already set transparency_cache to dirty, so maybe not need to check coverage separately).

@sparr
Copy link
Member

sparr commented Oct 18, 2024

On current master, I can try to sleep adjacent to a tree with seemingly no slowdown. Time advances in 5 minute steps while trying to fall asleep, 2-3 steps per second for me, a few seconds for the full half hour til "You have trouble sleeping".

Can you provide a more specific reproduction steps, or a simple unmodded save demonstrating this?

@RenechCDDA
Copy link
Member

@sparr Is the save in the OP not sufficient? I re-profiled it, it's still got the same issue. My linked PR which initially closed this issue might have shuffled around the exact hot path-ness, but it didn't end up resolving anything.

image

@sparr
Copy link
Member

sparr commented Oct 19, 2024

Discussion here and Discord and in #77026 suggests this problem might be specific to Windows / MSVC

@NetSysFire
Copy link
Member Author

Using the linux tiles build off github which is compiled using gcc9 on I think ubuntu 20.04 or 22.04, also on linux, has that issue for me which I just reproduced again. As such I doubt this is windows specific.

@PatrikLundell
Copy link
Contributor

The problem in #77026 is the same as this one. My attempt at profiling that one points at the same culprit(s) as @RenechCDDA has found above.

When it comes to specific OS/compilers, maybe it's the opposite, i.e. there may be ones that somehow are not seriously affected. Regardless, it's something that probably needs to be addressed (once someone can figure out how).

@IdleSol
Copy link
Contributor

IdleSol commented Oct 19, 2024

Okay, I'm not afraid to make a fool of myself.

auto &vision_transparency_cache = map_cache.vision_transparency_cache;

Do I understand correctly that vision_transparency_cache is a local copy of the cache? The one we make changes to:

vision_transparency_cache[loc.x()][loc.y()] = LIGHT_TRANSPARENCY_SOLID;

But these changes are only made to the copy, not the original. Shouldn't there be something along the lines of:

map_cache.vision_transparency_cache = vision_transparency_cache

Somewhere before return dirty.

And I'll ask again, why do we call this function, for each z level.

Cataclysm-DDA/src/map.cpp

Lines 9560 to 9562 in 6a8d75a

for( int z = minz; z <= maxz; z++ ) {
seen_cache_dirty |= build_vision_transparency_cache( z );
}

If for all levels except the one the character is on. The function does nothing?
if( p.z() != zlev ) {
return false;
}

I understand correctly that return interrupts the execution of the function, don't I?

Note. Isn't that why we have a theme: #77112

I'd like to remind you:

static move_mode_id previous_move_mode = player_character.current_movement_mode();

if ...
previous_move_mode != player_character.current_movement_mode() ) {

They're inside the same function. How can they not coincide? If the value setting and the comparison take place in the same call.

@x-qq
Copy link

x-qq commented Oct 19, 2024

Okay, I'm not afraid to make a fool of myself.

auto &vision_transparency_cache = map_cache.vision_transparency_cache;

Do I understand correctly that vision_transparency_cache is a local copy of the cache? The one we make changes to:

auto & creates a reference - there is no copying happening, vision_transparency_cache refers to the same memory as map_cache.vision_transparency_cache

@PatrikLundell
Copy link
Contributor

Yes, to me it looks like

        seen_cache_dirty |= build_vision_transparency_cache( z );
    }

could be replaced by
seen_cache_dirty |= build_vision_transparency_cache( get_player_character.posz() );
assuming the current behavior is to be retained. Alternatively, you can also remove the 'z' parameter and just use the PC Z level in the operation, as this is the only usage of the operation. Another alternative would be to pass the PC character to the operation, as the caller needs to get it anyway.

And, again, yes, I can't see the loop changing player_character, so there shouldn't be any movement mode changes. Thus, it seems previous_current_mode can be removed, together with its usages. You'd still have a change to LIGHT_TRANSPARENCY_SOLID if it wasn't set for the crouching, etc. case.

@SurFlurer
Copy link
Contributor

@PatrikLundell there's a reason to rebuild all vision transparency caches: #72542 FYI

@RenechCDDA
Copy link
Member

I would definitely say any attempt to resolve this should involve reverting #73590

@PatrikLundell
Copy link
Contributor

If there's a reason to rebuild all caches, then they should all be rebuilt (which may well be what reverting #73590 would accomplish).

And I would guess it won't help with the performance, but fast but incorrect is rather useless anyway...

@IdleSol
Copy link
Contributor

IdleSol commented Oct 19, 2024

there's a reason to rebuild all vision transparency caches: #72542 FYI

Okay, hold on.

We call the build_vision_transparency_cache function for each z level.

Cataclysm-DDA/src/map.cpp

Lines 9560 to 9562 in 6a8d75a

for( int z = minz; z <= maxz; z++ ) {
seen_cache_dirty |= build_vision_transparency_cache( z );
}

Inside the function, we update the cache. Based on what we specified earlier and #72542, it's this line:

auto &vision_transparency_cache = map_cache.vision_transparency_cache;

Right? But I don't see a zlev variable. It's a cache of all the levels at once? And we update it 21 times? Of those 21 times, only when p.z() = zlev are changes made. The rest of the times it works:

if( p.z() != zlev ) {
return false;

I didn't miss anything?

@PatrikLundell
Copy link
Contributor

PatrikLundell commented Oct 19, 2024

The fetching of the map_cache requests the one for the specified level, so yes, it's one cache per level.
level_cache &map_cache = get_cache( zlev );

By the way, if we are to update all levels then there's no reason to loop outside of the operation (in its single user), as that can be done internally.
That would, of course, only make sense if the skipping of all levels but the current one was removed. I haven't analyzed the PR that was suggested to be reverted, as it contains a lot of discussion, but that would presumably have to be understood before a reversion can be performed.

Edit: If the cache should be processed on all levels, the crouching stuff should still only be done only on the current level, as it uses the PC's position, and I don't think it makes sense to set the caches on other Z levels to the same as you'd have on the surrounding tiles (on the same level).

However, it should be noted that the code does indeed do something on other Z levels, due to the memcpy call to copy the contents on the transparency_cache onto the vision_transparency_cache, even if it bails out after that, so I'm wrong about it not doing anything in these cases.

You could also change the loop over points_in_radius to first check for crouching etc. and only do the PC tile if not, and then only check for coverage in the loop. That might actually speed things up in our problem cases, because I don't think the PC is actually lying down to sleep, and thus won't have to process the surrounding tiles.

Edit 2:
The first change of #73590 only sets the cache to dirty on a change to LIGHT_TRANSPARENCY_SOLID, rather than unconditionally (when crouching etc.).
The second change adds the seemingly pointless check and assignment if movement_mode if it differs from what seems to be its own value (as discussed above).

@IdleSol
Copy link
Contributor

IdleSol commented Oct 19, 2024

Another stupid question. How many loops are in this line? 9 or 27? A position consists of 3 coordinates, each can have 3 values (radius 1).

for( const tripoint_bub_ms &loc : points_in_radius( p, 1 ) ) {

A little further, the radius can be up to 60. How many cycles is that? 120^3?

for( const tripoint_bub_ms &loc : points_in_radius( p, MAX_VIEW_DISTANCE ) ) {

UPD. I'll explain why I'm fixated on this point. The game slows down not only when you sleep (prone position). But also when just waiting, provided the character is lying down (prone) or sitting (crouching).

So the cause must have something to do with it.

@PatrikLundell
Copy link
Contributor

PatrikLundell commented Oct 19, 2024

WRONG!!!
~~Very good question, and checking the implementation shows it's 27.
In the second case it's 21 * 121 * 121 (in the max view distance case), because the Z levels are clamped.

However, it seems we are in need of a points_in_radius_2D operation, because that's what we actually want here, and probably in a lot of other places too. This seems to actually be the root of the issue.~~

Correction.
There's the default parameter radiusz, defaulting to 0, which specifies the Z level extension, so it's only the current Z level unless this additional parameter is specified.
Thus, it's 3 * 3 and 121 * 121(at most) respectively.

@IdleSol
Copy link
Contributor

IdleSol commented Oct 19, 2024

I would suggest adding a debug message somewhere here:

if( vision_transparency_cache[loc.x()][loc.y()] != LIGHT_TRANSPARENCY_SOLID ||

            if( vision_transparency_cache[loc.x()][loc.y()] != LIGHT_TRANSPARENCY_SOLID ||
                previous_move_mode != player_character.current_movement_mode() ) {
                previous_move_mode = player_character.current_movement_mode();
                vision_transparency_cache[loc.x()][loc.y()] = LIGHT_TRANSPARENCY_SOLID;
                dirty = true;
++ debug message (...)
            }

Then see how many times this debug message appears in 10 seconds. Provided the character does not change his position. And is in a lying position next to any object that has a coverage greater than 30. That is, it satisfies the condition:

} else if( ( is_crouching || is_prone || low_profile ) && coverage( loc ) >= 30 ) {

This should confirm or deny the assumption that build_vision_transparency_cache always returns true. Provided the character is lying down.

Because if it returns true every turn, then there is a cache update every turn.

build_seen_cache( getlocal( p ), zlev, sr );

And another stupid question. The build_seen_cache function in the link above, passes 3 parameters. Does it expect 6?

Cataclysm-DDA/src/lightmap.cpp

Lines 1005 to 1006 in 3841a67

void map::build_seen_cache( const tripoint_bub_ms &origin, const int target_z, int extension_range,
bool cumulative, bool camera, int penalty )

That's probably normal, but I'll ask just in case.


UPD.

Deleted.

@PatrikLundell
Copy link
Contributor

We've been barking up the wrong tree!

The reason this is slow isn't because this particular code is slow, but rather because of
dirty = true;
being set every turn. Comment out that and it gets a lot faster (and wrong, of course). You'd get the same effect if there would be any translucent tile within the reality bubble while trying to fast forward time (the loop below).

Thus, the problem to solve would be to detect that the vision transparency cache actually hasn't changed, and thus that the cache isn't actually dirty.
I guess one way to do that would be to do the checks in several passes:

  • Go through the translucent loop and record all tiles that are translucent, as well as whether they were translucent before. Set "dirty" if any of those weren't LIGHT_TRANSPARENT_SOLID before.

  • Go through the tiles around the PC in a similar manner. Set "dirty" if any of those tiles weren't LIGHT_TRANSPARENCY_SOLID before, but should be now.

  • Do the raw copy of the cache.

  • Go through the list of transparent tiles and change those.

  • Go through the list of PC surrounding tiles and apply setting of LIGHT_TRANSPARENCY_SOLID as needed.

@PatrikLundell
Copy link
Contributor

PatrikLundell commented Oct 19, 2024

build_seen_cache has 4 parameters with default values, so not providing the last three is OK, as the defaults are used.

Some nutjob has decided that it's good practice for C(++?) to not display defaulted parameters in the implementation, but only in the declaration because providing the full profile is somehow cluttering the implementation presentation. Thus, you can never trust the implementation to show the full picture. There are checks in place that rejects your code if this info is provided.

Edit:
This code is a lot faster for the sleep case, and I think it still does the correct thing:

bool map::build_vision_transparency_cache( int zlev )
{
    level_cache &map_cache = get_cache( zlev );
    auto &transparency_cache = map_cache.transparency_cache;
    auto &vision_transparency_cache = map_cache.vision_transparency_cache;

    Character& player_character = get_player_character();
    const tripoint_bub_ms p = player_character.pos_bub();

    bool dirty = false;

    std::vector<tripoint_bub_ms> solid_tiles;

    if (p.z() == zlev) {
        // This segment handles vision when the player is crouching or prone. It only checks adjacent tiles.
        // If you change this, also consider creature::sees and map::obstacle_coverage.
        const bool is_crouching = player_character.is_crouching();
        const bool low_profile = player_character.has_effect(effect_quadruped_full) &&
            player_character.is_running();
        const bool is_prone = player_character.is_prone();

        if (is_crouching || is_prone || low_profile) {
            for (const tripoint_bub_ms& loc : points_in_radius(p, 1)) {
                if (loc != p && coverage(loc) >= 30) {
                    // If we're crouching or prone behind an obstacle, we can't see past it.
                    dirty |= vision_transparency_cache[loc.x()][loc.y()] != LIGHT_TRANSPARENCY_SOLID;
                    solid_tiles.emplace_back(loc);
                }
            }
        }
        
        // This segment handles blocking vision through TRANSLUCENT flagged terrain.
        for (const tripoint_bub_ms& loc : points_in_radius(p, MAX_VIEW_DISTANCE)) {
            if (loc != p && map::ter(loc).obj().has_flag(ter_furn_flag::TFLAG_TRANSLUCENT)) {
                solid_tiles.emplace_back(loc);
                dirty |= vision_transparency_cache[loc.x()][loc.y()] != LIGHT_TRANSPARENCY_SOLID;
            }
        }
    }

    memcpy( &vision_transparency_cache, &transparency_cache, sizeof( transparency_cache ) );

    if( p.z() != zlev ) {
        return false;
    }

    // The tile player is standing on should always be visible
    vision_transparency_cache[p.x()][p.y()] = LIGHT_TRANSPARENCY_OPEN_AIR;

    for (const tripoint_bub_ms loc : solid_tiles) {
        vision_transparency_cache[loc.x()][loc.y()] = LIGHT_TRANSPARENCY_SOLID;
    }

    return dirty;
}

@IdleSol
Copy link
Contributor

IdleSol commented Oct 20, 2024

  1. Does it matter in what order these lines go in? You have them in different places, in different order.
                    dirty |= vision_transparency_cache[loc.x()][loc.y()] != LIGHT_TRANSPARENCY_SOLID;
                    solid_tiles.emplace_back(loc);
  1. Does the if( p.z() != zlev ) check purposely come after if( p.z() == zlev )? Unless there's some point, I'd suggest swapping it around. Should be a little faster.

@PatrikLundell
Copy link
Contributor

  1. The order of the lines mentioned does not matter.
  2. The checks whether to dirty the cache have to be done while the old cache data is still present. Thus, it has to come before the memcpy operation that replaces it. The storage of the tiles to change is done because we need to apply the changes to the new cache contents, and thus the actual changes of the cache tiles have to be performed after the cache contents replacement. So, yes, it is important. We can't bail out before the cache replacement, but we have to collect data before it.
    However, it would be possible to move the not equal check above the declaration of the vectors and insert a memcpy call in it before returning and remove the now needless check for equality beneath it. That would be a bit faster.

@IdleSol
Copy link
Contributor

IdleSol commented Oct 20, 2024

Thanks for the clarification

@PatrikLundell
Copy link
Contributor

OK, this is essentially the same as the version posted above, with various minor tweaks that shouldn't affect functionality (but might be very marginally faster):

bool map::build_vision_transparency_cache( int zlev )
{
    level_cache &map_cache = get_cache( zlev );
    cata::mdarray<float, point_bub_ms> &transparency_cache = map_cache.transparency_cache;
    cata::mdarray<float, point_bub_ms> &vision_transparency_cache = map_cache.vision_transparency_cache;

    Character &player_character = get_player_character();
    const tripoint_bub_ms p = player_character.pos_bub();

    if( p.z() != zlev ) {
        // Just copy the transparency cache and be done with it.
        memcpy( &vision_transparency_cache, &transparency_cache, sizeof( transparency_cache ) );
        return false;
    }

    bool dirty = false;

    std::vector<tripoint_bub_ms> solid_tiles;

    // This segment handles vision when the player is crouching or prone. It only checks adjacent tiles.
    // If you change this, also consider creature::sees and map::obstacle_coverage.
    const bool is_crouching = player_character.is_crouching();
    const bool low_profile = player_character.has_effect( effect_quadruped_full ) &&
                             player_character.is_running();
    const bool is_prone = player_character.is_prone();

    if( is_crouching || is_prone || low_profile ) {
        for( const tripoint_bub_ms &loc : points_in_radius( p, 1 ) ) {
            if( loc != p && coverage( loc ) >= 30 ) {
                // If we're crouching or prone behind an obstacle, we can't see past it.
                dirty |= vision_transparency_cache[loc.x()][loc.y()] != LIGHT_TRANSPARENCY_SOLID;
                solid_tiles.emplace_back( loc );
            }
        }
    }

    // This segment handles blocking vision through TRANSLUCENT flagged terrain.
    for( const tripoint_bub_ms &loc : points_in_radius( p, MAX_VIEW_DISTANCE ) ) {
        if( map::ter( loc ).obj().has_flag( ter_furn_flag::TFLAG_TRANSLUCENT ) && loc != p ) {
            dirty |= vision_transparency_cache[loc.x()][loc.y()] != LIGHT_TRANSPARENCY_SOLID;
            solid_tiles.emplace_back( loc );
        }
    }

    memcpy( &vision_transparency_cache, &transparency_cache, sizeof( transparency_cache ) );

    // The tile player is standing on should always be visible
    vision_transparency_cache[p.x()][p.y()] = LIGHT_TRANSPARENCY_OPEN_AIR;

    for( const tripoint_bub_ms loc : solid_tiles ) {
        vision_transparency_cache[loc.x()][loc.y()] = LIGHT_TRANSPARENCY_SOLID;
    }

    return dirty;
}

I can make a PR of this, unless @IdleSol wants to do it.

I've subjected it to some minor testing:

  • Sleep as in the OP.
  • Debug change the time to 18:00 the next day (to have full light), lie down, and wait for 3 hours. The light levels decrease as expected.
  • Walked around a bit, with nothing odd seen in the visibility.

It can be noted that this only reverts the part of #73590 that doesn't do anything (comparing the PCs stance to a copy of the same stance. If anything, the change doubles down on that PRs approach of not dirtying the cache if nothing has changed by comparing to the previous cache state.

My understanding is that this code copies the current transparency cache, presumably just evaluated, and then adjusts it by blocking visibility through translucent terrain (light through, but not vision: stained/frosted glass, etc.), and obstructions due to stance. The vision cache should only be marked as dirty if these additions change the vision cache state, but not if it retains the state from the previous turn. Any changes caused by changes to the transparency cache should be dealt with elsewhere, as there's nothing in this code dealing with that.

@IdleSol
Copy link
Contributor

IdleSol commented Oct 20, 2024

I definitely don't want to. I mean, I don't even understand half of this stuff

@NetSysFire
Copy link
Member Author

Fixed in #77193.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Code: Performance Performance boosting code (CPU, memory, etc.) Fields / Furniture / Terrain / Traps Objects that are part of the map or its features. (S2 - Confirmed) Bug that's been confirmed to exist
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants