diff --git a/package-lock.json b/package-lock.json index a2f94e5a..7c19cd4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@next2d/player", - "version": "3.2.0", + "version": "3.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@next2d/player", - "version": "3.2.0", + "version": "3.3.0", "license": "MIT", "workspaces": [ "packages/*" @@ -19,14 +19,14 @@ "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", - "@typescript-eslint/eslint-plugin": "^8.59.1", - "@typescript-eslint/parser": "^8.59.1", + "@typescript-eslint/eslint-plugin": "^8.59.2", + "@typescript-eslint/parser": "^8.59.2", "@webgpu/types": "^0.1.69", "eslint": "^10.3.0", "eslint-plugin-unused-imports": "^4.4.1", "globals": "^17.6.0", "jsdom": "^29.1.1", - "rollup": "^4.60.2", + "rollup": "^4.60.3", "typescript": "^6.0.3", "vite": "^8.0.10", "vitest": "^4.1.5", @@ -1130,9 +1130,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", - "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", "cpu": [ "arm" ], @@ -1144,9 +1144,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", - "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", "cpu": [ "arm64" ], @@ -1158,9 +1158,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", - "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", "cpu": [ "arm64" ], @@ -1172,9 +1172,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", - "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", "cpu": [ "x64" ], @@ -1186,9 +1186,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", - "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", "cpu": [ "arm64" ], @@ -1200,9 +1200,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", - "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", "cpu": [ "x64" ], @@ -1214,9 +1214,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", - "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", "cpu": [ "arm" ], @@ -1231,9 +1231,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", - "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", "cpu": [ "arm" ], @@ -1248,9 +1248,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", - "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", "cpu": [ "arm64" ], @@ -1265,9 +1265,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", - "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", "cpu": [ "arm64" ], @@ -1282,9 +1282,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", - "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", "cpu": [ "loong64" ], @@ -1299,9 +1299,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", - "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", "cpu": [ "loong64" ], @@ -1316,9 +1316,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", - "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", "cpu": [ "ppc64" ], @@ -1333,9 +1333,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", - "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", "cpu": [ "ppc64" ], @@ -1350,9 +1350,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", - "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", "cpu": [ "riscv64" ], @@ -1367,9 +1367,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", - "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", "cpu": [ "riscv64" ], @@ -1384,9 +1384,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", - "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", "cpu": [ "s390x" ], @@ -1401,9 +1401,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", - "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", "cpu": [ "x64" ], @@ -1418,9 +1418,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", - "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", "cpu": [ "x64" ], @@ -1435,9 +1435,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", - "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", "cpu": [ "x64" ], @@ -1449,9 +1449,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", - "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", "cpu": [ "arm64" ], @@ -1463,9 +1463,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", - "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", "cpu": [ "arm64" ], @@ -1477,9 +1477,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", - "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", "cpu": [ "ia32" ], @@ -1491,9 +1491,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", - "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", "cpu": [ "x64" ], @@ -1505,9 +1505,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", - "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", "cpu": [ "x64" ], @@ -1583,17 +1583,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz", - "integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", + "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.1", - "@typescript-eslint/type-utils": "8.59.1", - "@typescript-eslint/utils": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/type-utils": "8.59.2", + "@typescript-eslint/utils": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1606,7 +1606,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.59.1", + "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -1622,16 +1622,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz", - "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz", + "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.59.1", - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/typescript-estree": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "engines": { @@ -1647,14 +1647,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz", - "integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", + "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.1", - "@typescript-eslint/types": "^8.59.1", + "@typescript-eslint/tsconfig-utils": "^8.59.2", + "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "engines": { @@ -1669,14 +1669,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz", - "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", + "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1" + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1687,9 +1687,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz", - "integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", + "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", "dev": true, "license": "MIT", "engines": { @@ -1704,15 +1704,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz", - "integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", + "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/typescript-estree": "8.59.1", - "@typescript-eslint/utils": "8.59.1", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1729,9 +1729,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz", - "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", + "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", "dev": true, "license": "MIT", "engines": { @@ -1743,16 +1743,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz", - "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", + "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.59.1", - "@typescript-eslint/tsconfig-utils": "8.59.1", - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1", + "@typescript-eslint/project-service": "8.59.2", + "@typescript-eslint/tsconfig-utils": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1810,16 +1810,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz", - "integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz", + "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.1", - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/typescript-estree": "8.59.1" + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1834,13 +1834,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz", - "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", + "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3216,9 +3216,9 @@ } }, "node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3472,9 +3472,9 @@ } }, "node_modules/postcss": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", - "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -3597,9 +3597,9 @@ } }, "node_modules/rollup": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", - "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", "dev": true, "license": "MIT", "dependencies": { @@ -3613,31 +3613,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.2", - "@rollup/rollup-android-arm64": "4.60.2", - "@rollup/rollup-darwin-arm64": "4.60.2", - "@rollup/rollup-darwin-x64": "4.60.2", - "@rollup/rollup-freebsd-arm64": "4.60.2", - "@rollup/rollup-freebsd-x64": "4.60.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", - "@rollup/rollup-linux-arm-musleabihf": "4.60.2", - "@rollup/rollup-linux-arm64-gnu": "4.60.2", - "@rollup/rollup-linux-arm64-musl": "4.60.2", - "@rollup/rollup-linux-loong64-gnu": "4.60.2", - "@rollup/rollup-linux-loong64-musl": "4.60.2", - "@rollup/rollup-linux-ppc64-gnu": "4.60.2", - "@rollup/rollup-linux-ppc64-musl": "4.60.2", - "@rollup/rollup-linux-riscv64-gnu": "4.60.2", - "@rollup/rollup-linux-riscv64-musl": "4.60.2", - "@rollup/rollup-linux-s390x-gnu": "4.60.2", - "@rollup/rollup-linux-x64-gnu": "4.60.2", - "@rollup/rollup-linux-x64-musl": "4.60.2", - "@rollup/rollup-openbsd-x64": "4.60.2", - "@rollup/rollup-openharmony-arm64": "4.60.2", - "@rollup/rollup-win32-arm64-msvc": "4.60.2", - "@rollup/rollup-win32-ia32-msvc": "4.60.2", - "@rollup/rollup-win32-x64-gnu": "4.60.2", - "@rollup/rollup-win32-x64-msvc": "4.60.2", + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", "fsevents": "~2.3.2" } }, diff --git a/package.json b/package.json index 396c5b21..cf3eb110 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@next2d/player", - "version": "3.2.1", - "description": "Experience the fast and beautiful anti-aliased rendering of WebGL. You can create rich, interactive graphics, cross-platform applications and games without worrying about browser or device compatibility.", + "version": "3.3.0", + "description": "Experience the fast and beautiful anti-aliased rendering of WebGL/WebGPU. You can create rich, interactive graphics, cross-platform applications and games without worrying about browser or device compatibility.", "author": "Toshiyuki Ienaga (https://github.com/ienaga/)", "license": "MIT", "homepage": "https://next2d.app", @@ -55,14 +55,14 @@ "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", - "@typescript-eslint/eslint-plugin": "^8.59.1", - "@typescript-eslint/parser": "^8.59.1", + "@typescript-eslint/eslint-plugin": "^8.59.2", + "@typescript-eslint/parser": "^8.59.2", "@webgpu/types": "^0.1.69", "eslint": "^10.3.0", "eslint-plugin-unused-imports": "^4.4.1", "globals": "^17.6.0", "jsdom": "^29.1.1", - "rollup": "^4.60.2", + "rollup": "^4.60.3", "typescript": "^6.0.3", "vite": "^8.0.10", "vitest": "^4.1.5", diff --git a/packages/cache/src/CacheStore.test.ts b/packages/cache/src/CacheStore.test.ts new file mode 100644 index 00000000..b29fb162 --- /dev/null +++ b/packages/cache/src/CacheStore.test.ts @@ -0,0 +1,78 @@ +import { CacheStore } from "./CacheStore"; +import { describe, expect, it, vi } from "vitest"; + +describe("CacheStore trash flow integration", () => +{ + it("removeTimer -> get -> removeTimerScheduledCache should still delete the entry", () => + { + vi.useFakeTimers(); + + const store = new CacheStore(); + store.set("12345", "0", { "value": "old" }); + + expect(store.has("12345", "0")).toBe(true); + + // removeTimer で削除予定に登録 + store.removeTimer("12345"); + expect((store as any)["_$trash"].has("12345")).toBe(true); + + // タイマー発火前に get が呼ばれても、削除予定は維持されない= + // 削除予定をキャンセルし、再度キャッシュとして有効化する + const value = store.get("12345", "0"); + expect(value).toEqual({ "value": "old" }); + expect((store as any)["_$trash"].has("12345")).toBe(false); + + // タイマー発火 (1 秒経過) + vi.advanceTimersByTime(1000); + expect(store.$removeCache).toBe(true); + + // get でキャンセルされたので removeTimerScheduledCache は何も削除しない + store.removeTimerScheduledCache(); + expect(store.has("12345", "0")).toBe(true); + expect(store.$removeIds.length).toBe(0); + + vi.useRealTimers(); + }); + + it("removeTimer -> (no get) -> removeTimerScheduledCache should delete the entry and push removeIds", () => + { + vi.useFakeTimers(); + + const store = new CacheStore(); + store.$removeIds.length = 0; + store.set("67890", "0", { "value": "stale" }); + + store.removeTimer("67890"); + expect((store as any)["_$trash"].has("67890")).toBe(true); + + vi.advanceTimersByTime(1000); + expect(store.$removeCache).toBe(true); + + store.removeTimerScheduledCache(); + + // 実際に削除されている + expect(store.has("67890", "0")).toBe(false); + expect(store.$removeIds).toContain(67890); + expect((store as any)["_$trash"].size).toBe(0); + + vi.useRealTimers(); + }); + + it("get on an entry not in trash_store should not affect trash_store of other entries", () => + { + vi.useFakeTimers(); + + const store = new CacheStore(); + store.set("aaa", "0", "alive"); + store.set("bbb", "0", "doomed"); + + store.removeTimer("bbb"); + expect((store as any)["_$trash"].has("bbb")).toBe(true); + + // 別エントリへの get は他エントリの削除予定に影響を与えない + expect(store.get("aaa", "0")).toBe("alive"); + expect((store as any)["_$trash"].has("bbb")).toBe(true); + + vi.useRealTimers(); + }); +}); diff --git a/packages/cache/src/CacheStore.ts b/packages/cache/src/CacheStore.ts index 2ba82a68..737ee211 100644 --- a/packages/cache/src/CacheStore.ts +++ b/packages/cache/src/CacheStore.ts @@ -9,6 +9,7 @@ import { execute as cacheStoreGenerateKeysService } from "./CacheStore/service/C import { execute as cacheStoreGenerateFilterKeysService } from "./CacheStore/service/CacheStoreGenerateFilterKeysService"; import { execute as cacheStoreRemoveTimerService } from "./CacheStore/service/CacheStoreRemoveTimerService"; import { execute as cacheStoreRemoveTimerScheduledCacheService } from "./CacheStore/service/CacheStoreRemoveTimerScheduledCacheService"; +import { execute as cacheStoreCancelRemoveTimerService } from "./CacheStore/service/CacheStoreCancelRemoveTimerService"; /** * @description キャッシュ管理クラス @@ -156,6 +157,20 @@ export class CacheStore cacheStoreRemoveTimerService(this, this._$store, this._$trash, id); } + /** + * @description 指定IDの削除タイマー登録を取り消す(再利用時に呼ぶ) + * Cancel the deletion timer registration for the specified ID (call on reuse) + * + * @param {string} id + * @returns {void} + * @method + * @public + */ + cancelRemoveTimer (id: string): void + { + cacheStoreCancelRemoveTimerService(this._$store, this._$trash, id); + } + /** * @description タイマーでセットされた削除フラグを持つIDをキャッシュストアから削除する * Remove the ID with the deletion flag set by the timer from the cache store @@ -209,7 +224,7 @@ export class CacheStore */ get (unique_key: string, key: string): any { - return cacheStoreGetService(this._$store, unique_key, key); + return cacheStoreGetService(this._$store, this._$trash, unique_key, key); } /** diff --git a/packages/cache/src/CacheStore/service/CacheStoreCancelRemoveTimerService.test.ts b/packages/cache/src/CacheStore/service/CacheStoreCancelRemoveTimerService.test.ts new file mode 100644 index 00000000..1da412ca --- /dev/null +++ b/packages/cache/src/CacheStore/service/CacheStoreCancelRemoveTimerService.test.ts @@ -0,0 +1,51 @@ +import { execute } from "./CacheStoreCancelRemoveTimerService"; +import { describe, expect, it } from "vitest"; + +describe("CacheStoreCancelRemoveTimerService.js test", () => +{ + it("test case1: trash_storeにidが存在しない場合は何もしない", () => + { + const data = new Map(); + const store = new Map(); + store.set("2", data); + const trash = new Map(); + + execute(store, trash, "2"); + + expect(trash.size).toBe(0); + expect(data.has("trash")).toBe(false); + }); + + it("test case2: trash_storeにidが存在する場合はtrash_storeから削除し、data_storeのtrashキーも削除する", () => + { + const data = new Map(); + data.set("trash", true); + + const store = new Map(); + store.set("2", data); + + const trash = new Map(); + trash.set("2", data); + + expect(trash.size).toBe(1); + expect(data.has("trash")).toBe(true); + + execute(store, trash, "2"); + + expect(trash.size).toBe(0); + expect(data.has("trash")).toBe(false); + }); + + it("test case3: trash_storeにidが存在するがdata_storeにidが存在しない場合はtrash_storeのみ削除する", () => + { + const store = new Map(); + const trash = new Map(); + trash.set("2", new Map()); + + expect(trash.size).toBe(1); + + execute(store, trash, "2"); + + expect(trash.size).toBe(0); + }); +}); diff --git a/packages/cache/src/CacheStore/service/CacheStoreCancelRemoveTimerService.ts b/packages/cache/src/CacheStore/service/CacheStoreCancelRemoveTimerService.ts new file mode 100644 index 00000000..31276c31 --- /dev/null +++ b/packages/cache/src/CacheStore/service/CacheStoreCancelRemoveTimerService.ts @@ -0,0 +1,35 @@ +/** + * @description 指定IDの削除タイマー登録を取り消す + * Cancel the deletion timer registration for the specified ID + * + * 削除タイマー(trash_store)に登録済みのエントリを再利用する場合に呼ぶ。 + * これにより、1秒後の removeTimerScheduledCache() による wipe を防ぐ。 + * 主にステージから一旦外れたインスタンスが addedToStage で再復帰した際の + * キャッシュ復活経路として使用する。 + * When reusing an entry that has been registered for removal in the trash_store, + * this is called to prevent the wipe by removeTimerScheduledCache() after 1 second. + * + * @param {Map} data_store + * @param {Map} trash_store + * @param {string} id + * @return {void} + * @method + * @public + */ +export const execute = ( + data_store: Map>, + trash_store: Map>, + id: string +): void => { + + if (!trash_store.has(id)) { + return ; + } + + trash_store.delete(id); + + const data = data_store.get(id); + if (data) { + data.delete("trash"); + } +}; diff --git a/packages/cache/src/CacheStore/service/CacheStoreGetService.test.ts b/packages/cache/src/CacheStore/service/CacheStoreGetService.test.ts index ce72a409..c9e3d558 100644 --- a/packages/cache/src/CacheStore/service/CacheStoreGetService.test.ts +++ b/packages/cache/src/CacheStore/service/CacheStoreGetService.test.ts @@ -6,14 +6,16 @@ describe("CacheStoreGetService.js test", () => it("test case1", () => { const store = new Map(); - expect(execute(store, "1", "0")).toBe(null); + const trash = new Map(); + expect(execute(store, trash, "1", "0")).toBe(null); }); it("test case2", () => { const store = new Map(); + const trash = new Map(); store.set("1", new Map()); - expect(execute(store, "1", "0")).toBe(null); + expect(execute(store, trash, "1", "0")).toBe(null); }); it("test case3", () => @@ -22,7 +24,58 @@ describe("CacheStoreGetService.js test", () => data.set("0", "test"); const store = new Map(); + const trash = new Map(); store.set("1", data); - expect(execute(store, "1", "0")).toBe("test"); + expect(execute(store, trash, "1", "0")).toBe("test"); + }); + + it("get should cancel pending deletion when entry is in trash_store", () => + { + const data = new Map(); + data.set("0", "test"); + data.set("trash", true); + + const store = new Map(); + store.set("1", data); + + const trash = new Map(); + trash.set("1", data); + + expect(trash.has("1")).toBe(true); + expect(data.has("trash")).toBe(true); + + const result = execute(store, trash, "1", "0"); + + // 値は問題なく取得できる + expect(result).toBe("test"); + + // 削除予定はキャンセルされる + expect(trash.has("1")).toBe(false); + expect(data.has("trash")).toBe(false); + + // 元データは削除されていない + expect(store.has("1")).toBe(true); + expect(data.get("0")).toBe("test"); + }); + + it("get should not touch trash_store for entries that are not pending deletion", () => + { + const data = new Map(); + data.set("0", "test"); + + const otherData = new Map(); + otherData.set("trash", true); + + const store = new Map(); + store.set("1", data); + + const trash = new Map(); + trash.set("2", otherData); + + // 別 id のエントリへの get は trash_store を触らない + execute(store, trash, "1", "0"); + + expect(trash.has("2")).toBe(true); + expect(otherData.has("trash")).toBe(true); }); }); diff --git a/packages/cache/src/CacheStore/service/CacheStoreGetService.ts b/packages/cache/src/CacheStore/service/CacheStoreGetService.ts index 98d5cde7..759b06dd 100644 --- a/packages/cache/src/CacheStore/service/CacheStoreGetService.ts +++ b/packages/cache/src/CacheStore/service/CacheStoreGetService.ts @@ -2,15 +2,25 @@ * @description 指定のキーからデータを取得 * Get data from the specified key * + * 削除予定(trash_store 登録済み)のエントリは get 時に削除予定をキャンセルする。 + * これにより、get 後の removeTimerScheduledCache() で + * 内側 Map の "trash" マーク削除によりループがスキップされる問題を回避する。 + * When entries are pending deletion (registered in trash_store), the + * scheduled removal is canceled here. This prevents the + * removeTimerScheduledCache() loop from skipping such entries due to + * the inner Map's "trash" marker having been deleted. + * * @param {Map} data_store + * @param {Map} trash_store * @param {string} unique_key * @param {string} key * @return {*} * @method -* @public -*/ + * @public + */ export const execute = ( data_store: Map>, + trash_store: Map>, unique_key: string, key: string ): any => { @@ -20,6 +30,10 @@ export const execute = ( return null; } - data.delete("trash"); + if (trash_store.has(unique_key)) { + trash_store.delete(unique_key); + data.delete("trash"); + } + return data.get(key) || null; -}; \ No newline at end of file +}; diff --git a/packages/display/src/DisplayObject/service/DisplayObjectDispatchAddedToStageEventService.ts b/packages/display/src/DisplayObject/service/DisplayObjectDispatchAddedToStageEventService.ts index 8e3e8cc8..93f88f89 100644 --- a/packages/display/src/DisplayObject/service/DisplayObjectDispatchAddedToStageEventService.ts +++ b/packages/display/src/DisplayObject/service/DisplayObjectDispatchAddedToStageEventService.ts @@ -1,6 +1,7 @@ import type { DisplayObject } from "../../DisplayObject"; import { Event } from "@next2d/events"; import { $stageAssignedMap } from "../../DisplayObjectUtil"; +import { $cacheStore } from "@next2d/cache"; /** * @description DisplayObjectのADDED_TO_STAGEイベントを実行 @@ -19,6 +20,14 @@ export const execute = (display_object: D): void => return ; } + // 一旦ステージから外れて削除タイマーに乗ったインスタンスがそのまま再復帰した場合、 + // 1秒後の wipe で巻き添えにならないよう trash 登録を取り消す。 + // ナビゲーション(毎回新インスタンス生成)では trash に該当 id がないため no-op。 + if (display_object.uniqueKey) { + $cacheStore.cancelRemoveTimer(display_object.uniqueKey); + } + $cacheStore.cancelRemoveTimer(`${display_object.instanceId}`); + display_object.$addedToStage = true; if (display_object.willTrigger(Event.ADDED_TO_STAGE)) { display_object.dispatchEvent(new Event(Event.ADDED_TO_STAGE)); diff --git a/packages/display/src/DisplayObject/service/DisplayObjectDispatchRemovedToStageEventService.ts b/packages/display/src/DisplayObject/service/DisplayObjectDispatchRemovedToStageEventService.ts index 826bc448..e6cbd60e 100644 --- a/packages/display/src/DisplayObject/service/DisplayObjectDispatchRemovedToStageEventService.ts +++ b/packages/display/src/DisplayObject/service/DisplayObjectDispatchRemovedToStageEventService.ts @@ -31,4 +31,16 @@ export const execute = (display_object: D): void => ) { $cacheStore.removeTimer(display_object.uniqueKey); } + + // instanceId ベースキャッシュの cleanup + // - コンテナの filter/cacheAsBitmap/blend (Main に "filterKey"/"bitmapKey" あり) → removeTimer (1秒猶予で再復帰可) + // - Shape/Text/Video の filter (ContextApplyFilterUseCase が Worker 側 "fKey"/"fTexture"/"offsetX"/"offsetY" を格納) + // は Main 側にエントリがないため、$removeIds に直接 push して Worker 側の GPU リソースを解放する。 + // どちらも放置するとナビゲーション繰り返しでアトラス/GPU メモリが枯渇する。 + const instanceIdKey = `${display_object.instanceId}`; + if ($cacheStore.has(instanceIdKey)) { + $cacheStore.removeTimer(instanceIdKey); + } else { + $cacheStore.$removeIds.push(display_object.instanceId); + } }; \ No newline at end of file diff --git a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts index 67418821..14106a18 100644 --- a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts +++ b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts @@ -338,10 +338,11 @@ export const execute =

( const filterKey = $cacheStore.generateFilterKeys( tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3] ); - const filterCache = $cacheStore.get( + const cachedFilterKey = $cacheStore.get( `${display_object_container.instanceId}`, - `${filterKey}` + "filterKey" ); + const filterCache = cachedFilterKey === filterKey; let updated = false; const params = []; @@ -393,36 +394,38 @@ export const execute =

( const layerWidth = Math.ceil(Math.abs(layerBounds[2] - layerBounds[0]) * layerScaleX); const layerHeight = Math.ceil(Math.abs(layerBounds[3] - layerBounds[1]) * layerScaleY); - if (filterCache) { + if (filterCache && !updated) { // キャッシュがあって、変更がなければキャッシュを使用 - if (!updated) { - renderQueue.push(1, - layerWidth, layerHeight, - 1, 1, display_object_container.instanceId, filterKey, - bounds[0], bounds[1], bounds[2], bounds[3], - tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerBounds[0], layerBounds[1], - layerScaleX, layerScaleY, - tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], - tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7] - ); - - $poolBoundsArray(layerBounds); - $poolBoundsArray(bounds); - - if (tColorTransform !== color_transform) { - ColorTransform.release(tColorTransform); - } - if (tMatrix !== matrix) { - Matrix.release(tMatrix); - } - return ; - } + renderQueue.push(1, + layerWidth, layerHeight, + 1, 1, display_object_container.instanceId, filterKey, + bounds[0], bounds[1], bounds[2], bounds[3], + tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerBounds[0], layerBounds[1], + layerScaleX, layerScaleY, + tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], + tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7] + ); - // どこかで変更があったので、キャッシュを削除 - $cacheStore.removeById(`${display_object_container.instanceId}`); + $poolBoundsArray(layerBounds); + $poolBoundsArray(bounds); + + if (tColorTransform !== color_transform) { + ColorTransform.release(tColorTransform); + } + if (tMatrix !== matrix) { + Matrix.release(tMatrix); + } + return ; } + // filterCache && updated の場合は MISS パスへフォールスルー。 + // Main は直後の set("filterKey", ...) で上書きされ、Worker 側は + // ContextContainerEndLayerUseCase 内で旧 fTexture を解放してから + // 新しいテクスチャを保存するため、ここでの removeById/$removeIds.push は + // 不要。push してしまうと、Worker が同フレームで作成した新キャッシュを + // 次フレーム冒頭で wipe してしまい、その次の HIT で無描画になる。 + renderQueue.push( 1, layerWidth, layerHeight, @@ -461,7 +464,7 @@ export const execute =

( $cacheStore.set( `${display_object_container.instanceId}`, - `${filterKey}`, true + "filterKey", filterKey ); } else { diff --git a/packages/display/src/MovieClip/service/MovieClipGetChildrenService.ts b/packages/display/src/MovieClip/service/MovieClipGetChildrenService.ts index 9721a783..0377068e 100644 --- a/packages/display/src/MovieClip/service/MovieClipGetChildrenService.ts +++ b/packages/display/src/MovieClip/service/MovieClipGetChildrenService.ts @@ -116,6 +116,16 @@ export const execute = ( $cacheStore.removeTimer(displayObject.uniqueKey); } + // コンテナの instanceId ベースキャッシュ → removeTimer (1秒猶予) + // Shape/Text/Video の filter キャッシュ (Main にエントリなし) → 直接 $removeIds.push + // どちらも Worker 側の GPU リソースを解放する。 + const instanceIdKey = `${displayObject.instanceId}`; + if ($cacheStore.has(instanceIdKey)) { + $cacheStore.removeTimer(instanceIdKey); + } else { + $cacheStore.$removeIds.push(displayObject.instanceId); + } + if (displayObject.willTrigger(Event.REMOVED)) { displayObject.dispatchEvent( new Event(Event.REMOVED, true) diff --git a/packages/display/src/Shape/usecase/ShapeClearBitmapBufferUseCase.ts b/packages/display/src/Shape/usecase/ShapeClearBitmapBufferUseCase.ts index 8cf3f87c..81011d28 100644 --- a/packages/display/src/Shape/usecase/ShapeClearBitmapBufferUseCase.ts +++ b/packages/display/src/Shape/usecase/ShapeClearBitmapBufferUseCase.ts @@ -20,9 +20,12 @@ export const execute = (shape: Shape): void => shape.graphics.clear(); // cache clear + // Main 側のみ wipe する。Worker 側は次回 MISS 描画時に + // 旧 Node を解放してから新 Node を確保するため、ここで $removeIds.push しない。 + // push してしまうと、Worker が同フレームで作成した新キャッシュを次フレーム冒頭で + // wipe → その次のフレームで Main HIT → Worker null → 永続無描画 になる。 if (shape.uniqueKey !== "" && $cacheStore.has(shape.uniqueKey)) { $cacheStore.removeById(shape.uniqueKey); - $cacheStore.$removeIds.push(+shape.uniqueKey); } // apply changes diff --git a/packages/renderer/src/Command/service/CommandRemoveCacheService.test.ts b/packages/renderer/src/Command/service/CommandRemoveCacheService.test.ts index 877b6efd..c34aad40 100644 --- a/packages/renderer/src/Command/service/CommandRemoveCacheService.test.ts +++ b/packages/renderer/src/Command/service/CommandRemoveCacheService.test.ts @@ -15,7 +15,8 @@ vi.mock("@next2d/cache", () => { vi.mock("../../RendererUtil", () => ({ "$context": { - "removeNode": vi.fn() + "removeNode": vi.fn(), + "releaseTextureCache": vi.fn() } })); @@ -39,7 +40,7 @@ describe("CommandRemoveCacheService.js test", () => { expect($cacheStore.has).toHaveBeenCalledWith("1"); expect($cacheStore.getById).toHaveBeenCalledWith("1"); - expect($context.removeNode).toHaveBeenCalledTimes(2); + expect($context.releaseTextureCache).toHaveBeenCalledTimes(2); expect($cacheStore.removeById).toHaveBeenCalledWith("1"); }); diff --git a/packages/renderer/src/Command/service/CommandRemoveCacheService.ts b/packages/renderer/src/Command/service/CommandRemoveCacheService.ts index 71330581..0f742c0e 100644 --- a/packages/renderer/src/Command/service/CommandRemoveCacheService.ts +++ b/packages/renderer/src/Command/service/CommandRemoveCacheService.ts @@ -20,8 +20,9 @@ export const execute = (remove_cache_keys: Float32Array): void => } const cache = $cacheStore.getById(cacheKey); - for (const node of cache.values()) { - $context.removeNode(node); + for (const value of cache.values()) { + // Node / ITextureObject / IAttachmentObject を型判定して適切に解放(プリミティブはスキップ) + $context.releaseTextureCache(value); } $cacheStore.removeById(cacheKey); diff --git a/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts index 26a00d74..98237f2b 100644 --- a/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts +++ b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts @@ -71,7 +71,12 @@ export const execute = (render_queue: Float32Array, index: number): number => const width = Math.ceil(Math.abs(xMax - xMin)); const height = Math.ceil(Math.abs(yMax - yMin)); - // fixed logic + // ShapeClearBitmapBufferUseCase 等で Main 側のみ wipe された場合、 + // Worker には旧 Node が残っているため、新規作成前に解放してアトラスリーク防止 + const oldBitmapNode = $cacheStore.get(uniqueKey, `${cacheKey}`) as Node | null; + if (oldBitmapNode) { + $context.removeNode(oldBitmapNode); + } node = $context.createNode(width, height); $cacheStore.set(uniqueKey, `${cacheKey}`, node); @@ -109,7 +114,12 @@ export const execute = (render_queue: Float32Array, index: number): number => const width = Math.ceil(Math.abs(xMax - xMin) * xScale); const height = Math.ceil(Math.abs(yMax - yMin) * yScale); - // fixed logic + // ShapeClearBitmapBufferUseCase 等で Main 側のみ wipe された場合、 + // Worker には旧 Node が残っているため、新規作成前に解放してアトラスリーク防止 + const oldNode = $cacheStore.get(uniqueKey, `${cacheKey}`) as Node | null; + if (oldNode) { + $context.removeNode(oldNode); + } node = $context.createNode(width, height); $cacheStore.set(uniqueKey, `${cacheKey}`, node); diff --git a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts index 540be8f6..77fae555 100644 --- a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts +++ b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts @@ -63,11 +63,16 @@ export const execute = (render_queue: Float32Array, index: number): number => const hasNode = Boolean(render_queue[index++]); - node = hasNode - ? $cacheStore.get(uniqueKey, `${cacheKey}`) as Node - : $context.createNode(width, height); - - if (!hasNode) { + if (hasNode) { + node = $cacheStore.get(uniqueKey, `${cacheKey}`) as Node; + } else { + // TextFieldResetUseCase 等で Main 側のみ wipe された場合、 + // Worker には旧 Node が残っているため、新規作成前に解放してアトラスリーク防止 + const oldNode = $cacheStore.get(uniqueKey, `${cacheKey}`) as Node | null; + if (oldNode) { + $context.removeNode(oldNode); + } + node = $context.createNode(width, height); $cacheStore.set(uniqueKey, `${cacheKey}`, node); } diff --git a/packages/renderer/src/Video/usecase/VideoRenderUseCase.test.ts b/packages/renderer/src/Video/usecase/VideoRenderUseCase.test.ts index c90b2ad8..99de315d 100644 --- a/packages/renderer/src/Video/usecase/VideoRenderUseCase.test.ts +++ b/packages/renderer/src/Video/usecase/VideoRenderUseCase.test.ts @@ -16,6 +16,7 @@ vi.mock("../../RendererUtil", () => ({ "reset": vi.fn(), "setTransform": vi.fn(), "createNode": vi.fn(() => mockNode), + "removeNode": vi.fn(), "beginNodeRendering": vi.fn(), "endNodeRendering": vi.fn(), "drawElement": vi.fn(), diff --git a/packages/renderer/src/Video/usecase/VideoRenderUseCase.ts b/packages/renderer/src/Video/usecase/VideoRenderUseCase.ts index 472e874e..061dc765 100644 --- a/packages/renderer/src/Video/usecase/VideoRenderUseCase.ts +++ b/packages/renderer/src/Video/usecase/VideoRenderUseCase.ts @@ -54,11 +54,16 @@ export const execute = ( const hasNode = Boolean(render_queue[index++]); - node = hasNode - ? $cacheStore.get(uniqueKey, `${cacheKey}`) as Node - : $context.createNode(width, height); - - if (!hasNode) { + if (hasNode) { + node = $cacheStore.get(uniqueKey, `${cacheKey}`) as Node; + } else { + // Main 側のみ wipe された race を考慮し、新規作成前に旧 Node を解放 + // (Shape/TextField MISS パスと同じ防御策。通常は no-op) + const oldNode = $cacheStore.get(uniqueKey, `${cacheKey}`) as Node | null; + if (oldNode) { + $context.removeNode(oldNode); + } + node = $context.createNode(width, height); $cacheStore.set(uniqueKey, `${cacheKey}`, node); } diff --git a/packages/text/src/TextField/usecase/TextFieldResetUseCase.ts b/packages/text/src/TextField/usecase/TextFieldResetUseCase.ts index 54ea99b2..dc4ed67f 100644 --- a/packages/text/src/TextField/usecase/TextFieldResetUseCase.ts +++ b/packages/text/src/TextField/usecase/TextFieldResetUseCase.ts @@ -18,8 +18,11 @@ export const execute = (text_field: TextField): void => textFieldApplyChangesService(text_field); // Remove cache + // Main 側のみ wipe する。Worker 側は次回 MISS 描画時に + // 旧 Node を解放してから新 Node を確保するため、ここで $removeIds.push しない。 + // push してしまうと、Worker が同フレームで作成した新キャッシュを次フレーム冒頭で + // wipe → その次のフレームで Main HIT → Worker null → 永続無描画 になる。 if (text_field.uniqueKey !== "" && $cacheStore.has(text_field.uniqueKey)) { $cacheStore.removeById(text_field.uniqueKey); - $cacheStore.$removeIds.push(+text_field.uniqueKey); } }; \ No newline at end of file diff --git a/packages/webgl/src/Context.ts b/packages/webgl/src/Context.ts index 31b68a61..76eff025 100644 --- a/packages/webgl/src/Context.ts +++ b/packages/webgl/src/Context.ts @@ -1,6 +1,7 @@ import type { IAttachmentObject } from "./interface/IAttachmentObject"; import type { IBlendMode } from "./interface/IBlendMode"; import type { IBounds } from "./interface/IBounds"; +import type { ITextureObject } from "./interface/ITextureObject"; import type { Node } from "@next2d/texture-packer"; import { execute as beginPath } from "./PathCommand/service/PathCommandBeginPathService"; import { execute as moveTo } from "./PathCommand/usecase/PathCommandMoveToUseCase"; @@ -29,6 +30,7 @@ import { execute as contextUseGridService } from "./Context/service/ContextUseGr import { execute as contextClipUseCase } from "./Context/usecase/ContextClipUseCase"; import { execute as atlasManagerCreateNodeService } from "./AtlasManager/service/AtlasManagerCreateNodeService"; import { execute as atlasManagerRemoveNodeService } from "./AtlasManager/service/AtlasManagerRemoveNodeService"; +import { execute as textureManagerReleaseTextureObjectUseCase } from "./TextureManager/usecase/TextureManagerReleaseTextureObjectUseCase"; import { execute as blnedDrawDisplayObjectUseCase } from "./Blend/usecase/BlnedDrawDisplayObjectUseCase"; import { execute as blnedClearArraysInstancedUseCase } from "./Blend/usecase/BlnedClearArraysInstancedUseCase"; import { execute as blnedDrawArraysInstancedUseCase } from "./Blend/usecase/BlnedDrawArraysInstancedUseCase"; @@ -380,6 +382,30 @@ export class Context atlasManagerRemoveNodeService(node); } + /** + * @description CacheStore に格納された任意の値を解放する。 + * Release any value stored in CacheStore. + * + * CommandRemoveCacheService から instanceId / uniqueKey 単位で呼ばれ、 + * その map に含まれる Node / ITextureObject / プリミティブの全てを + * 安全に解放できるよう型判定で振り分ける。 + * プリミティブ(fKey の数値など)はスキップする。 + * + * @param {*} value + * @return {void} + */ + releaseTextureCache (value: any): void + { + if (!value || typeof value !== "object") { + return ; + } + if ("w" in value) { + atlasManagerRemoveNodeService(value as Node); + } else if ("resource" in value) { + textureManagerReleaseTextureObjectUseCase(value as ITextureObject); + } + } + beginNodeRendering (node: Node): void { this.newDrawState = true; diff --git a/packages/webgl/src/Context/usecase/ContextApplyFilterUseCase.ts b/packages/webgl/src/Context/usecase/ContextApplyFilterUseCase.ts index 867cdb20..09345974 100644 --- a/packages/webgl/src/Context/usecase/ContextApplyFilterUseCase.ts +++ b/packages/webgl/src/Context/usecase/ContextApplyFilterUseCase.ts @@ -82,14 +82,21 @@ export const execute = ( let useCache = false; const fKey = $cacheStore.get(unique_key, "fKey"); - if (fKey === key) { - const cacheTextureObject = $cacheStore.get(unique_key, "fTexture") as ITextureObject; + const cacheTextureObject = $cacheStore.get(unique_key, "fTexture") as ITextureObject | null; + if (fKey === key && cacheTextureObject) { if (updated) { + // 同 filterKey で更新あり: 旧テクスチャを解放して再生成 textureManagerReleaseTextureObjectUseCase(cacheTextureObject); } else { + // 同 filterKey で更新なし: キャッシュをそのまま再利用 useCache = true; textureObject = cacheTextureObject; } + } else if (cacheTextureObject) { + // filterKey が変わった(matrix 変化等)場合に旧テクスチャを解放してから上書き。 + // これがないと、フィルター付き Shape のスケール/回転アニメーション中に + // 毎フレーム GPU テクスチャがリークする。 + textureManagerReleaseTextureObjectUseCase(cacheTextureObject); } let offsetX = 0; diff --git a/packages/webgl/src/Context/usecase/ContextContainerEndLayerUseCase.ts b/packages/webgl/src/Context/usecase/ContextContainerEndLayerUseCase.ts index f5888bad..bb4cd204 100644 --- a/packages/webgl/src/Context/usecase/ContextContainerEndLayerUseCase.ts +++ b/packages/webgl/src/Context/usecase/ContextContainerEndLayerUseCase.ts @@ -237,8 +237,12 @@ export const execute = ( // cacheAsBitmap の仕様としてスクリーン上も cacheScale 倍のサイズで // 表示されるため、ここでの縮小は行わずそのまま main に合成する。 - // キャッシュに保存 + // キャッシュに保存(古いfTextureを先に解放してからGPUリーク防止) if (unique_key) { + const oldTexture = $cacheStore.get(unique_key, "fTexture") as ITextureObject | null; + if (oldTexture) { + textureManagerReleaseTextureObjectUseCase(oldTexture); + } $cacheStore.set(unique_key, "fKey", filter_key); $cacheStore.set(unique_key, "fTexture", textureObject); } diff --git a/packages/webgpu/src/Context.ts b/packages/webgpu/src/Context.ts index 508293c0..46cf98b0 100644 --- a/packages/webgpu/src/Context.ts +++ b/packages/webgpu/src/Context.ts @@ -3110,6 +3110,33 @@ export class Context } } + /** + * @description CacheStore に格納された任意の値を解放する。 + * Release any value stored in CacheStore. + * + * CommandRemoveCacheService から instanceId / uniqueKey 単位で呼ばれ、 + * Node / IAttachmentObject / プリミティブの全てを安全に解放できるよう + * 型判定で振り分ける。 + * + * @param {*} value + * @return {void} + */ + releaseTextureCache (value: any): void + { + if (!value || typeof value !== "object") { + return ; + } + if ("w" in value) { + const node = value as Node; + const rootNode = $rootNodes[node.index]; + if (rootNode) { + rootNode.dispose(node.x, node.y, node.w, node.h); + } + } else if ("color" in value || "texture" in value) { + this.frameBufferManager.releaseTemporaryAttachment(value as IAttachmentObject); + } + } + /** * @description フレームバッファの描画情報をキャンバスに転送 * Transfer frame buffer contents to the canvas diff --git a/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts index 0108fe43..40b3258d 100644 --- a/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts @@ -706,8 +706,12 @@ export const execute = ( filterAttachment, matrix, params, devicePixelRatio, config ); - // キャッシュに保存 + // キャッシュに保存(古いfTextureを先に解放してGPUリーク防止) if (unique_key) { + const oldAttachment = $cacheStore.get(unique_key, "fTexture") as IAttachmentObject | null; + if (oldAttachment) { + config.frameBufferManager.releaseTemporaryAttachment(oldAttachment); + } $cacheStore.set(unique_key, "fKey", filter_key); $cacheStore.set(unique_key, "fTexture", filterAttachment); } diff --git a/packages/webgpu/src/FillTexturePool.test.ts b/packages/webgpu/src/FillTexturePool.test.ts new file mode 100644 index 00000000..15c8fce3 --- /dev/null +++ b/packages/webgpu/src/FillTexturePool.test.ts @@ -0,0 +1,250 @@ +import { + $acquireFillTexture, + $releaseFillTexture, + $acquireRenderTexture, + $releaseRenderTexture, + $clearFillTexturePool, + $getOrCreateView +} from "./FillTexturePool"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +interface IMockTexture { + width: number; + height: number; + destroyed: boolean; + destroy: () => void; + createView: () => object; +} + +const createMockDevice = () => +{ + return { + "createTexture": vi.fn(({ size }: { size: { width: number; height: number } }) => + { + const tex: IMockTexture = { + "width": size.width, + "height": size.height, + "destroyed": false, + "destroy": vi.fn(function (this: IMockTexture) { this.destroyed = true; }), + "createView": vi.fn(() => ({})) + }; + return tex; + }) + } as unknown as GPUDevice; +}; + +beforeEach(() => +{ + // モジュール状態をクリーンに + $clearFillTexturePool(); +}); + +describe("FillTexturePool basic acquire/release", () => +{ + it("acquire creates a new texture when pool is empty", () => + { + const device = createMockDevice(); + const texture = $acquireFillTexture(device, 256, 1); + + expect(device.createTexture).toHaveBeenCalledTimes(1); + expect((texture as unknown as IMockTexture).width).toBe(256); + expect((texture as unknown as IMockTexture).height).toBe(1); + }); + + it("release then acquire returns the same texture (bucket reuse)", () => + { + const device = createMockDevice(); + const t1 = $acquireFillTexture(device, 128, 128); + $releaseFillTexture(t1); + + const t2 = $acquireFillTexture(device, 128, 128); + expect(t2).toBe(t1); + // 2 度目は新規作成されない + expect(device.createTexture).toHaveBeenCalledTimes(1); + }); + + it("different sizes use different buckets", () => + { + const device = createMockDevice(); + const a = $acquireFillTexture(device, 256, 1); + const b = $acquireFillTexture(device, 512, 1); + $releaseFillTexture(a); + $releaseFillTexture(b); + + const reused = $acquireFillTexture(device, 256, 1); + expect(reused).toBe(a); + // 別バケットには影響しない + const reusedB = $acquireFillTexture(device, 512, 1); + expect(reusedB).toBe(b); + }); +}); + +describe("FillTexturePool bucket size limit", () => +{ + it("destroys texture when bucket exceeds $MAX_BUCKET_SIZE (32)", () => + { + const device = createMockDevice(); + const textures: IMockTexture[] = []; + for (let idx = 0; idx < 33; ++idx) { + const t = $acquireFillTexture(device, 64, 64) as unknown as IMockTexture; + textures.push(t); + } + // 33 個を release + for (const t of textures) { + $releaseFillTexture(t as unknown as GPUTexture); + } + + // 32 個までは保持、33 個目は destroy される + const destroyedCount = textures.filter((t) => t.destroyed).length; + expect(destroyedCount).toBe(1); + expect(textures[32].destroyed).toBe(true); + }); +}); + +describe("FillTexturePool total count limit", () => +{ + it("destroys texture when total exceeds $MAX_TOTAL (256) across multiple buckets", () => + { + const device = createMockDevice(); + const textures: IMockTexture[] = []; + + // 9 種類の (width, height) × 32 = 288 個生成 + for (let bucket = 0; bucket < 9; ++bucket) { + const w = 100 + bucket; + for (let idx = 0; idx < 32; ++idx) { + const t = $acquireFillTexture(device, w, 1) as unknown as IMockTexture; + textures.push(t); + } + } + + for (const t of textures) { + $releaseFillTexture(t as unknown as GPUTexture); + } + + // 各バケット 32 で計 288。バケット上限ピッタリ + 総数 256 の組合せ判定: + // - バケット 1~8: 各 32 push → 計 256 で総数到達 + // - バケット 9 の 32 個は全て総数上限により destroy + const destroyedCount = textures.filter((t) => t.destroyed).length; + expect(destroyedCount).toBe(32); + }); +}); + +describe("FillTexturePool render pool independence", () => +{ + it("fill and render pools track counts independently", () => + { + const device = createMockDevice(); + const fill = $acquireFillTexture(device, 32, 32) as unknown as IMockTexture; + const render = $acquireRenderTexture(device, 32, 32) as unknown as IMockTexture; + + $releaseFillTexture(fill as unknown as GPUTexture); + $releaseRenderTexture(render as unknown as GPUTexture); + + // 別々のプールに格納されており再取得時に両方使い回される + const fillReused = $acquireFillTexture(device, 32, 32); + const renderReused = $acquireRenderTexture(device, 32, 32); + expect(fillReused).toBe(fill); + expect(renderReused).toBe(render); + }); + + it("render pool also enforces bucket limit", () => + { + const device = createMockDevice(); + const textures: IMockTexture[] = []; + for (let idx = 0; idx < 33; ++idx) { + const t = $acquireRenderTexture(device, 200, 200) as unknown as IMockTexture; + textures.push(t); + } + for (const t of textures) { + $releaseRenderTexture(t as unknown as GPUTexture); + } + const destroyedCount = textures.filter((t) => t.destroyed).length; + expect(destroyedCount).toBe(1); + }); +}); + +describe("FillTexturePool clear", () => +{ + it("destroys all pooled textures and resets internal counts", () => + { + const device = createMockDevice(); + + // 10 個 acquire → release で fill プールに 10 個保持 + const fillTextures: IMockTexture[] = []; + for (let idx = 0; idx < 10; ++idx) { + fillTextures.push($acquireFillTexture(device, 256, 256) as unknown as IMockTexture); + } + for (const t of fillTextures) { + $releaseFillTexture(t as unknown as GPUTexture); + } + + // 10 個 acquire → release で render プールに 10 個保持 + const renderTextures: IMockTexture[] = []; + for (let idx = 0; idx < 10; ++idx) { + renderTextures.push($acquireRenderTexture(device, 256, 256) as unknown as IMockTexture); + } + for (const t of renderTextures) { + $releaseRenderTexture(t as unknown as GPUTexture); + } + + $clearFillTexturePool(); + + // 全 texture が destroy 済み + for (const t of fillTextures) { + expect(t.destroyed).toBe(true); + } + for (const t of renderTextures) { + expect(t.destroyed).toBe(true); + } + + // クリア後は新規 acquire で createTexture が呼ばれることで内部カウンタもリセット済みと検証 + const callsBefore = (device.createTexture as ReturnType).mock.calls.length; + $acquireFillTexture(device, 256, 256); + const callsAfter = (device.createTexture as ReturnType).mock.calls.length; + expect(callsAfter).toBe(callsBefore + 1); + }); + + it("clear after exceeding limits also resets total count tracking", () => + { + const device = createMockDevice(); + + // バケット上限を超えて release(一部 destroy される) + const textures: IMockTexture[] = []; + for (let idx = 0; idx < 40; ++idx) { + textures.push($acquireFillTexture(device, 16, 16) as unknown as IMockTexture); + } + for (const t of textures) { + $releaseFillTexture(t as unknown as GPUTexture); + } + + $clearFillTexturePool(); + + // クリア後、新たに 32 個 release できる(カウンタが 0 に戻っている) + const fresh: IMockTexture[] = []; + for (let idx = 0; idx < 32; ++idx) { + fresh.push($acquireFillTexture(device, 16, 16) as unknown as IMockTexture); + } + for (const t of fresh) { + $releaseFillTexture(t as unknown as GPUTexture); + } + + // この 32 個は全て保持される(destroy されていない) + const destroyedFresh = fresh.filter((t) => t.destroyed).length; + expect(destroyedFresh).toBe(0); + }); +}); + +describe("FillTexturePool $getOrCreateView", () => +{ + it("returns the same view for the same texture across multiple calls", () => + { + const device = createMockDevice(); + const tex = $acquireFillTexture(device, 64, 64); + + const v1 = $getOrCreateView(tex); + const v2 = $getOrCreateView(tex); + + expect(v1).toBe(v2); + expect((tex as unknown as IMockTexture).createView).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/webgpu/src/FillTexturePool.ts b/packages/webgpu/src/FillTexturePool.ts index d6c977bb..2cf888e6 100644 --- a/packages/webgpu/src/FillTexturePool.ts +++ b/packages/webgpu/src/FillTexturePool.ts @@ -36,6 +36,20 @@ const $FILL_TEXTURE_USAGE = 0x06; */ const $RENDER_TEXTURE_USAGE = 0x16; +/** + * @description 同一サイズのバケットあたりの最大保持数 + * Maximum number of textures held per same-size bucket + * @type {number} + */ +const $MAX_BUCKET_SIZE = 32; + +/** + * @description プール全体での最大保持数(fill/render それぞれに適用) + * Maximum number of textures held in the entire pool (applied independently to fill/render) + * @type {number} + */ +const $MAX_TOTAL = 256; + /** * @description 塗りテクスチャのオブジェクトプール * Object pool for fill textures @@ -43,6 +57,13 @@ const $RENDER_TEXTURE_USAGE = 0x16; */ const $pool: Map = new Map(); +/** + * @description 塗りテクスチャプールの総数 + * Total count of textures currently held in the fill pool + * @type {number} + */ +let $fillTotalCount = 0; + /** * @description レンダーテクスチャのオブジェクトプール * Object pool for render textures @@ -50,6 +71,13 @@ const $pool: Map = new Map(); */ const $renderPool: Map = new Map(); +/** + * @description レンダーテクスチャプールの総数 + * Total count of textures currently held in the render pool + * @type {number} + */ +let $renderTotalCount = 0; + /** * @description プールから塗りテクスチャを取得、なければ新規作成 * Acquire a fill texture from the pool, or create a new one @@ -63,6 +91,7 @@ export const $acquireFillTexture = (device: GPUDevice, width: number, height: nu const key = `${width}_${height}`; const list = $pool.get(key); if (list && list.length > 0) { + $fillTotalCount--; return list.pop()!; } return device.createTexture({ @@ -73,8 +102,8 @@ export const $acquireFillTexture = (device: GPUDevice, width: number, height: nu }; /** - * @description 塗りテクスチャをプールに返却 - * Release a fill texture back to the pool + * @description 塗りテクスチャをプールに返却。上限超過時は destroy する + * Release a fill texture back to the pool. Destroyed when limits are exceeded * @param {GPUTexture} texture * @return {void} */ @@ -86,7 +115,14 @@ export const $releaseFillTexture = (texture: GPUTexture): void => list = []; $pool.set(key, list); } + + if (list.length >= $MAX_BUCKET_SIZE || $fillTotalCount >= $MAX_TOTAL) { + texture.destroy(); + return; + } + list.push(texture); + $fillTotalCount++; }; /** @@ -102,6 +138,7 @@ export const $acquireRenderTexture = (device: GPUDevice, width: number, height: const key = `${width}_${height}`; const list = $renderPool.get(key); if (list && list.length > 0) { + $renderTotalCount--; return list.pop()!; } return device.createTexture({ @@ -112,8 +149,8 @@ export const $acquireRenderTexture = (device: GPUDevice, width: number, height: }; /** - * @description レンダーテクスチャをプールに返却 - * Release a render texture back to the pool + * @description レンダーテクスチャをプールに返却。上限超過時は destroy する + * Release a render texture back to the pool. Destroyed when limits are exceeded * @param {GPUTexture} texture * @return {void} */ @@ -125,7 +162,14 @@ export const $releaseRenderTexture = (texture: GPUTexture): void => list = []; $renderPool.set(key, list); } + + if (list.length >= $MAX_BUCKET_SIZE || $renderTotalCount >= $MAX_TOTAL) { + texture.destroy(); + return; + } + list.push(texture); + $renderTotalCount++; }; /** @@ -141,6 +185,7 @@ export const $clearFillTexturePool = (): void => } } $pool.clear(); + $fillTotalCount = 0; for (const [, list] of $renderPool) { for (const texture of list) { @@ -148,4 +193,5 @@ export const $clearFillTexturePool = (): void => } } $renderPool.clear(); + $renderTotalCount = 0; }; diff --git a/src/index.ts b/src/index.ts index a5a3e57f..6005bfda 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import { Next2D } from "@next2d/core"; if (!("next2d" in window)) { - console.log("%c Next2D Player %c 3.2.1 %c https://next2d.app", + console.log("%c Next2D Player %c 3.3.0 %c https://next2d.app", "color: #fff; background: #5f5f5f", "color: #fff; background: #4bc729", "");