diff --git a/src/zm_zone.cpp b/src/zm_zone.cpp index 47e390030..ca810b75e 100644 --- a/src/zm_zone.cpp +++ b/src/zm_zone.cpp @@ -862,9 +862,10 @@ bool Zone::ParsePercentagePolygon(const char *poly_string, unsigned int width, u int32 px_x = static_cast(std::lround(pct_x * mon_w / 100.0)); int32 px_y = static_cast(std::lround(pct_y * mon_h / 100.0)); - // Clamp to monitor bounds - px_x = std::clamp(px_x, static_cast(0), static_cast(width)); - px_y = std::clamp(px_y, static_cast(0), static_cast(height)); + // Clamp to monitor bounds. Max valid pixel index is width-1/height-1; + // values equal to width/height cause out-of-bounds warnings in the rasterizer. + px_x = std::clamp(px_x, static_cast(0), static_cast(width) - 1); + px_y = std::clamp(px_y, static_cast(0), static_cast(height) - 1); Debug(3, "Percentage coord %.2f,%.2f -> pixel %d,%d", pct_x, pct_y, px_x, px_y); vertices.emplace_back(px_x, px_y); diff --git a/tests/zm_zone.cpp b/tests/zm_zone.cpp index a56d0cd45..42d6375e1 100644 --- a/tests/zm_zone.cpp +++ b/tests/zm_zone.cpp @@ -28,9 +28,9 @@ TEST_CASE("Zone::ParsePercentagePolygon: full-frame zone at 1920x1080", "[Zone]" REQUIRE(result == true); REQUIRE(polygon.GetVertices().size() == 4); REQUIRE(polygon.GetVertices()[0] == Vector2(0, 0)); - REQUIRE(polygon.GetVertices()[1] == Vector2(1920, 0)); - REQUIRE(polygon.GetVertices()[2] == Vector2(1920, 1080)); - REQUIRE(polygon.GetVertices()[3] == Vector2(0, 1080)); + REQUIRE(polygon.GetVertices()[1] == Vector2(1919, 0)); + REQUIRE(polygon.GetVertices()[2] == Vector2(1919, 1079)); + REQUIRE(polygon.GetVertices()[3] == Vector2(0, 1079)); } TEST_CASE("Zone::ParsePercentagePolygon: center 50% zone", "[Zone]") { @@ -68,8 +68,8 @@ TEST_CASE("Zone::ParsePercentagePolygon: different resolution", "[Zone]") { 640, 480, polygon); REQUIRE(result == true); - REQUIRE(polygon.GetVertices()[1] == Vector2(640, 0)); - REQUIRE(polygon.GetVertices()[2] == Vector2(640, 480)); + REQUIRE(polygon.GetVertices()[1] == Vector2(639, 0)); + REQUIRE(polygon.GetVertices()[2] == Vector2(639, 479)); } TEST_CASE("Zone::ParsePercentagePolygon: clamping beyond 100%", "[Zone]") { @@ -79,8 +79,8 @@ TEST_CASE("Zone::ParsePercentagePolygon: clamping beyond 100%", "[Zone]") { 1920, 1080, polygon); REQUIRE(result == true); - // 110% should be clamped to monitor width - REQUIRE(polygon.GetVertices()[1].x_ == 1920); + // 110% should be clamped to max valid pixel index (width-1) + REQUIRE(polygon.GetVertices()[1].x_ == 1919); } TEST_CASE("Zone::ParsePercentagePolygon: triangle", "[Zone]") { @@ -114,9 +114,9 @@ TEST_CASE("Zone::ParsePercentagePolygon: integer coords still work", "[Zone]") { REQUIRE(result == true); REQUIRE(polygon.GetVertices()[0] == Vector2(0, 0)); - REQUIRE(polygon.GetVertices()[1] == Vector2(1920, 0)); - REQUIRE(polygon.GetVertices()[2] == Vector2(1920, 1080)); - REQUIRE(polygon.GetVertices()[3] == Vector2(0, 1080)); + REQUIRE(polygon.GetVertices()[1] == Vector2(1919, 0)); + REQUIRE(polygon.GetVertices()[2] == Vector2(1919, 1079)); + REQUIRE(polygon.GetVertices()[3] == Vector2(0, 1079)); } TEST_CASE("Zone::ParsePolygonString: basic pixel parsing", "[Zone]") { @@ -166,9 +166,9 @@ TEST_CASE("Zone::ParsePercentagePolygon: percentage to pixel conversion", "[Zone auto const &verts = polygon.GetVertices(); REQUIRE(verts.size() == 4); REQUIRE(verts[0] == Vector2(0, 0)); - REQUIRE(verts[1] == Vector2(1920, 0)); - REQUIRE(verts[2] == Vector2(1920, 1080)); - REQUIRE(verts[3] == Vector2(0, 1080)); + REQUIRE(verts[1] == Vector2(1919, 0)); + REQUIRE(verts[2] == Vector2(1919, 1079)); + REQUIRE(verts[3] == Vector2(0, 1079)); } SECTION("50% rectangle converts to half-resolution pixels") { @@ -184,16 +184,16 @@ TEST_CASE("Zone::ParsePercentagePolygon: percentage to pixel conversion", "[Zone } SECTION("values are clamped to monitor bounds") { - // 100% should clamp to exact monitor dimensions + // 100% should clamp to max valid pixel index (width-1, height-1) bool ok = Zone::ParsePercentagePolygon("0,0 100,0 100,100 0,100", width, height, polygon); REQUIRE(ok); auto const &verts = polygon.GetVertices(); for (auto const &v : verts) { REQUIRE(v.x_ >= 0); - REQUIRE(v.x_ <= static_cast(width)); + REQUIRE(v.x_ < static_cast(width)); REQUIRE(v.y_ >= 0); - REQUIRE(v.y_ <= static_cast(height)); + REQUIRE(v.y_ < static_cast(height)); } } } @@ -213,12 +213,12 @@ TEST_CASE("Zone: pixel values through ParsePercentagePolygon produce wrong resul auto const &verts = polygon.GetVertices(); REQUIRE(verts.size() == 4); - // 639% of 1920 = 12268.8 -> clamped to 1920 - // 479% of 1080 = 5173.2 -> clamped to 1080 + // 639% of 1920 = 12268.8 -> clamped to width-1 + // 479% of 1080 = 5173.2 -> clamped to height-1 // All non-zero coords get clamped to monitor bounds — the zone is // degenerate (covers the full monitor instead of a sub-region) - REQUIRE(verts[1] == Vector2(static_cast(width), 0)); - REQUIRE(verts[2] == Vector2(static_cast(width), static_cast(height))); + REQUIRE(verts[1] == Vector2(static_cast(width) - 1, 0)); + REQUIRE(verts[2] == Vector2(static_cast(width) - 1, static_cast(height) - 1)); } // --- Auto-detect format tests ---