godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
91.12k stars 21.19k forks source link

Engine freezes when manipulating many `Polygon2D` #78599

Open 4d49 opened 1 year ago

4d49 commented 1 year ago

Godot version

v4.1.beta3.official [ada712e06]

System information

Godot v4.1.beta3 - Windows 10.0.22621 - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 2060 (NVIDIA; 31.0.15.3161) - AMD Ryzen 5 2600X Six-Core Processor (12 Threads)

Issue description

If you create many nodes (256x128) of the Polygon2D class and start changing them, then the engine will freeze for 8-9 times. It doesn't matter if you delete and re-create nodes or just change the polygon.

Steps to reproduce

Creates many nodes (256x128) of the Polygon2D class. Сreate a function that will change the "polygon" and the material of these nodes. At 8-9 times the engine will freeze for a while. I think the problem is somewhere in RenderingServer because if you enable OpenGL then the problem goes away.

Minimal reproduction project

space - update delete - rebuild

freeze-issue.zip

AThousandShips commented 1 year ago

I'm unsure of the reasonability of doing this scale of changes, this is 32768 nodes, being manipulated in a significant way, and unsure of how reasonable it is to expect it to not cause performance issues, some investigation is required

freeze for 8-9 times

What does this mean?

4d49 commented 1 year ago

I'm unsure of the reasonability of doing this scale of changes, this is 32768 nodes, being manipulated in a significant way, and unsure of how reasonable it is to expect it to not cause performance issues, some investigation is required

I understand this and in my project I use a custom class that calls RenderingServer and PhysicsServer directly. I don't use nodes for this, but I ran into a similar issue. Polygon2D is just for example.

What does this mean?

Excuse me. I meant the problem appears after 8-9 times as I call the function to change all Polygon2D. Just press "space" 10 times in the test project.

AThousandShips commented 1 year ago

Nodes aside, you're still manipulating a lot of data in the rendering and physics server, I don't think that's expected to work unless you use instancing, not individual objects manipulated on their own, as you used that type as an example you get no instancing, each has its own set of points

freeze for a while.

Roughly how long? A minute? Ten?

4d49 commented 1 year ago

Roughly how long? A minute? Ten?

msec изображение

AThousandShips commented 1 year ago

That's pretty expected to me at least, taking on the order of hundreds of microseconds per polygon here

4d49 commented 1 year ago

That's pretty expected to me at least, taking on the order of hundreds of microseconds per polygon here

But why does the problem appear only after a certain number of iterations? Some kind of memory cleanup? And why is there no problem on OpenGl?

AThousandShips commented 1 year ago

But why does the problem appear only after a certain number of iterations?

Unsure, could be memory related, could be some factor in your code triggering only at some points, could be that it is unreliable if the next process frame happens before or after updating the polygons

And why is there no problem on OpenGl?

Probably because either: 1) The feature is not fully supported in some way 2) The Forward+ renderer uses clustering which has some overhead to build data

AThousandShips commented 1 year ago

Does this happen if you use other means to generate these shapes? Like MeshInstance2D? I suspect Polygon2D might not be as performant, also creating a polygon like this does some math behind the scenes to decompose it into triangles which might also be a performance consideration

My main question is: Is this representative of any real world usage you are doing? Or is this just a stress test? Because I have a hard time seeing this as something that affects people in normal cases

Looking at your example you should use other means to draw hexagons if they are regular like this, use something like MeshInstance2D using the same mesh for each instance but separate materials, this is not the way to do this specific case, it takes much more memory and processing

Edit: I don't get any variation in the time, I get a steady around 900 ms each time, with at most 100 ms variation, and I'm using a debug build so likely much faster in release build

AThousandShips commented 1 year ago

So to summarize at least my views and thoughts: If you are wanting to do this kind of game structure, with a lot of hex tiles, then I'd recommend using either TileMap or MeshInstance2D, to do your drawing, I'd say that using just a square with a texture that has transparency is better here than a hex shape directly as it speeds up a lot of rendering, even if you are using just flat colors you can easily use a custom shader that takes a hex texture with white and transparent and just multiplying the color of the texture by the desired color as a shader uniform

This should even be possible to do with a MultimeshInstance2D which would help with a lot I'd suspect, though I haven't used it much myself so not sure how to do the specifics

TileMap is pretty much the perfect fit for this case though, it's literally what it is built for, and it supports hex tiles

If on the other hand this is just for performance testing, I'd say that this isn't unexpected, and that this massive updating of data is bound to create slowdowns

I hope this helps either way, and will keep this open for any other perspectives on if this is in fact some bug in the system, there could be a bottleneck in the rendering system making this less performant, but I suspect this isn't unexpected in general

4d49 commented 1 year ago

I continued experimenting and found strange behavior. For minimal reproduction project I used below function to create hexagon polygons:

static func create_hexagon(x: int, y: int) -> PackedVector2Array:
    const OUTER_RADIUS : float = 8.0
    const INNER_RADIUS : float = OUTER_RADIUS * cos(deg_to_rad(30.0))
    # Just 2D hexagon.
    const HEXAGON : PackedVector2Array = [
        Vector2(0.0, OUTER_RADIUS),
        Vector2(INNER_RADIUS, OUTER_RADIUS * 0.5),
        Vector2(INNER_RADIUS, OUTER_RADIUS * -.5),
        Vector2(0.0, -OUTER_RADIUS),
        Vector2(-INNER_RADIUS, OUTER_RADIUS * -.5),
        Vector2(-INNER_RADIUS, OUTER_RADIUS * 0.5),
    ]

    var position := Vector2(INNER_RADIUS * 2.0 * x, OUTER_RADIUS * 1.5 * y)

    if y % 2: # Add offset every second row.
        position.x += INNER_RADIUS

    return HEXAGON * Transform2D(0.0, -position) # Apply position.

And after several iterations of changing all polygons, I get freeze: изображение

In my project, instead of hexagons, I use the Voronoi diagram, but I ran into the same problem there too. But as soon as I added the 7th vertex to the hexagon:

const HEXAGON : PackedVector2Array = [
    Vector2(0.0, OUTER_RADIUS),
    Vector2(INNER_RADIUS, OUTER_RADIUS * 0.5),
    Vector2(INNER_RADIUS, OUTER_RADIUS * -.5),
    Vector2(0.0, -OUTER_RADIUS),
    Vector2(-INNER_RADIUS, OUTER_RADIUS * -.5),
    Vector2(-INNER_RADIUS, OUTER_RADIUS * 0.5),
    Vector2(0.0, OUTER_RADIUS), # Seven vertex...
]

Freeze is gone: изображение 🙈🙈🙈

4d49 commented 1 year ago

Does this happen if you use other means to generate these shapes? Like MeshInstance2D? I suspect Polygon2D might not be as performant, also creating a polygon like this does some math behind the scenes to decompose it into triangles which might also be a performance consideration

The problem is not with Polygon2D. For example, here is a custom class for creating polygons. Inside a class that works with RenderingServer directly. And there is the same problem.

class PolygonCanvas extends Object:
    var _canvas : RID
    var _mesh : RID

    var _vertices : PackedVector2Array
    var _material : Material

    func _init(parent: RID) -> void:
        _canvas = RenderingServer.canvas_item_create()
        RenderingServer.canvas_item_set_parent(_canvas, parent)

        _mesh = RenderingServer.mesh_create()
        RenderingServer.canvas_item_add_mesh(_canvas, _mesh)

custom-class.zip

4d49 commented 1 year ago

In a custom class to create meshes, I use this code:

    func set_vertices(vertices: PackedVector2Array) -> void:
        var arrays : Array = []
        arrays.resize(RenderingServer.ARRAY_MAX)
        arrays[RenderingServer.ARRAY_VERTEX] = vertices
        arrays[RenderingServer.ARRAY_TEX_UV] = vertices
        arrays[RenderingServer.ARRAY_INDEX] = Geometry2D.triangulate_polygon(vertices)

        RenderingServer.mesh_clear(_mesh)
        RenderingServer.mesh_add_surface_from_arrays(_mesh, RenderingServer.PRIMITIVE_TRIANGLES, arrays, [], {}, RenderingServer.ARRAY_FLAG_USE_2D_VERTICES)

I used ARRAY_INDEX. In OpenGL it all works fine. But if change the code like this:

    func set_vertices(vertices: PackedVector2Array) -> void:
        var indices : PackedInt32Array = Geometry2D.triangulate_polygon(vertices)

        var trinagulated := PackedVector2Array()
        trinagulated.resize(indices.size())

        for i in indices.size():
            trinagulated[i] = vertices[indices[i]]

        var arrays : Array = []
        arrays.resize(RenderingServer.ARRAY_MAX)
        arrays[RenderingServer.ARRAY_VERTEX] = trinagulated
        arrays[RenderingServer.ARRAY_TEX_UV] = trinagulated
        # Removed ARRAY_INDEX.

        RenderingServer.mesh_clear(_mesh)
        RenderingServer.mesh_add_surface_from_arrays(_mesh, RenderingServer.PRIMITIVE_TRIANGLES, arrays, [], {}, RenderingServer.ARRAY_FLAG_USE_2D_VERTICES)

Freeze is gone... Polygon2D also uses triangulation for indexing. So I think this is the problem.

Calinou commented 1 year ago

This is likely a duplicate of https://github.com/godotengine/godot/issues/74540, although your issue mention that you don't get performance issues when using the Compatibility rendering method (while https://github.com/godotengine/godot/issues/74540 claims that it does impact Compatibility).

4d49 commented 1 year ago

This is likely a duplicate of #74540, although your issue mention that you don't get performance issues when using the Compatibility rendering method (while #74540 claims that it does impact Compatibility).

That's right, the problem is only with Vulkan and ONLY if the mesh's vertices are indexed.

4d49 commented 1 year ago

The problem also applies to 3D.