fix: 离屏画布没有删除导致的内存泄漏

This commit is contained in:
unanmed 2024-11-20 21:11:01 +08:00
parent 003369bed1
commit f1b20450aa
15 changed files with 179 additions and 87 deletions

View File

@ -1,4 +1,5 @@
import { EventEmitter } from 'eventemitter3';
import { logger } from '../common/logger';
interface OffscreenCanvasEvent {
/** 当被动触发resize时例如core.domStyle.scale变化、窗口大小变化时触发使用size函数并不会触发 */
@ -26,6 +27,12 @@ export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
/** 更新标识符,如果发生变化则说明画布被动清空 */
symbol: number = 0;
private _freezed: boolean = false;
/** 当前画布是否被冻结 */
get freezed() {
return this._freezed;
}
/**
*
* @param alpha
@ -33,6 +40,7 @@ export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
*/
constructor(alpha: boolean = true, canvas?: HTMLCanvasElement) {
super();
// console.trace();
this.canvas = canvas ?? document.createElement('canvas');
this.ctx = this.canvas.getContext('2d', { alpha })!;
@ -46,6 +54,10 @@ export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
*
*/
size(width: number, height: number) {
if (this._freezed) {
logger.warn(33);
return;
}
let ratio = this.highResolution ? devicePixelRatio : 1;
const scale = core.domStyle.scale;
if (this.autoScale) {
@ -69,6 +81,10 @@ export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
* core.domStyle.scale
*/
withGameScale(auto: boolean) {
if (this._freezed) {
logger.warn(33);
return;
}
this.autoScale = auto;
this.size(this.width, this.height);
}
@ -77,6 +93,10 @@ export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
*
*/
setHD(hd: boolean) {
if (this._freezed) {
logger.warn(33);
return;
}
this.highResolution = hd;
this.size(this.width, this.height);
}
@ -85,6 +105,10 @@ export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
* 齿
*/
setAntiAliasing(anti: boolean) {
if (this._freezed) {
logger.warn(33);
return;
}
this.antiAliasing = anti;
this.ctx.imageSmoothingEnabled = anti;
}
@ -93,6 +117,10 @@ export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
*
*/
clear() {
if (this._freezed) {
logger.warn(33);
return;
}
this.ctx.save();
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
@ -103,13 +131,21 @@ export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
*
*/
delete() {
this.canvas.remove();
this.ctx.reset();
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
MotaOffscreenCanvas2D.list.delete(this);
}
freeze() {
this._freezed = true;
MotaOffscreenCanvas2D.list.delete(this);
}
/**
* Canvas2D对象
* @param canvas MotaOffscreenCanvas2D对象
* @returns
* @returns
*/
static clone(canvas: MotaOffscreenCanvas2D): MotaOffscreenCanvas2D {
const newCanvas = new MotaOffscreenCanvas2D();
@ -123,12 +159,13 @@ export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
canvas.width,
canvas.height
);
newCanvas.freeze();
return newCanvas;
}
static refreshAll() {
static refreshAll(force: boolean = false) {
this.list.forEach(v => {
if (v.autoScale) {
if (force || v.autoScale) {
v.size(v.width, v.height);
v.symbol++;
v.emit('resize');

View File

@ -480,6 +480,7 @@ function splitAutotiles(map: IdToNumber): AutotileCaches {
master.size(32, 32);
master.ctx.drawImage(img, 0, 0, 32, 32, 0, 0, 32, 32);
masterMap[auto] = master.canvas.toDataURL('image/png');
master.delete();
// 自动图块的绘制信息
for (let i = 0; i <= 0b11111111; i++) {
@ -504,6 +505,7 @@ function splitAutotiles(map: IdToNumber): AutotileCaches {
canvas.setAntiAliasing(false);
canvas.withGameScale(false);
canvas.size(32 * frame, 32);
canvas.freeze();
const ctx = canvas.ctx;
for (let i = 0; i < frame; i++) {
const dx = 32 * i;
@ -525,16 +527,17 @@ function splitAutotiles(map: IdToNumber): AutotileCaches {
}
}
const judge = new MotaOffscreenCanvas2D();
judge.setHD(false);
judge.setAntiAliasing(false);
judge.withGameScale(false);
judge.size(32, 32);
// 进行父子关系判断
for (const [key, img] of Object.entries(core.material.images.autotile)) {
const auto = map[key as AllIdsOf<'autotile'>];
// 只针对3*4的图块进行截取第一行中间的然后判断
const judge = new MotaOffscreenCanvas2D();
judge.setHD(false);
judge.setAntiAliasing(false);
judge.withGameScale(false);
judge.size(32, 32);
judge.clear();
judge.ctx.drawImage(img, 32, 0, 32, 32, 0, 0, 32, 32);
const data = judge.canvas.toDataURL('image/png');
@ -547,6 +550,7 @@ function splitAutotiles(map: IdToNumber): AutotileCaches {
}
}
}
judge.delete();
return cache as AutotileCaches;
}

View File

@ -4,28 +4,28 @@ import { logger } from '../common/logger';
import { Transform } from './transform';
import EventEmitter from 'eventemitter3';
export interface CameraTranslate {
export interface ICameraTranslate {
readonly type: 'translate';
readonly from: Camera;
x: number;
y: number;
}
export interface CameraRotate {
export interface ICameraRotate {
readonly type: 'rotate';
readonly from: Camera;
/** 旋转角,单位弧度 */
angle: number;
}
export interface CameraScale {
export interface ICameraScale {
readonly type: 'scale';
readonly from: Camera;
x: number;
y: number;
}
type CameraOperation = CameraTranslate | CameraScale | CameraRotate;
type CameraOperation = ICameraTranslate | ICameraScale | ICameraRotate;
interface CameraEvent {
destroy: [];
@ -145,8 +145,8 @@ export class Camera extends EventEmitter<CameraEvent> {
*
* @returns
*/
addTranslate(): CameraTranslate {
const item: CameraTranslate = {
addTranslate(): ICameraTranslate {
const item: ICameraTranslate = {
type: 'translate',
x: 0,
y: 0,
@ -160,8 +160,8 @@ export class Camera extends EventEmitter<CameraEvent> {
*
* @returns
*/
addRotate(): CameraRotate {
const item: CameraRotate = {
addRotate(): ICameraRotate {
const item: ICameraRotate = {
type: 'rotate',
angle: 0,
from: this
@ -174,8 +174,8 @@ export class Camera extends EventEmitter<CameraEvent> {
*
* @returns
*/
addScale(): CameraScale {
const item: CameraScale = {
addScale(): ICameraScale {
const item: ICameraScale = {
type: 'scale',
x: 1,
y: 1,
@ -213,7 +213,7 @@ export class Camera extends EventEmitter<CameraEvent> {
* @param time
*/
applyTranslateAnimation(
operation: CameraTranslate,
operation: ICameraTranslate,
animate: Animation,
time: number
) {
@ -237,7 +237,7 @@ export class Camera extends EventEmitter<CameraEvent> {
* @param time
*/
applyRotateAnimation(
operation: CameraRotate,
operation: ICameraRotate,
animate: Animation,
time: number
) {
@ -260,7 +260,7 @@ export class Camera extends EventEmitter<CameraEvent> {
* @param time
*/
applyScaleAnimation(
operation: CameraScale,
operation: ICameraScale,
animate: Animation,
time: number
) {
@ -284,7 +284,7 @@ export class Camera extends EventEmitter<CameraEvent> {
* @param time
*/
applyTranslateTransition(
operation: CameraTranslate,
operation: ICameraTranslate,
animate: Transition,
time: number
) {
@ -308,7 +308,7 @@ export class Camera extends EventEmitter<CameraEvent> {
* @param time
*/
applyRotateTransition(
operation: CameraRotate,
operation: ICameraRotate,
animate: Transition,
time: number
) {
@ -331,7 +331,7 @@ export class Camera extends EventEmitter<CameraEvent> {
* @param time
*/
applyScaleTransition(
operation: CameraScale,
operation: ICameraScale,
animate: Transition,
time: number
) {
@ -514,7 +514,7 @@ export class CameraAnimation extends EventEmitter<CameraAnimationEvent> {
* @param timing
*/
translate(
operation: CameraTranslate,
operation: ICameraTranslate,
x: number,
y: number,
time: number,
@ -542,7 +542,7 @@ export class CameraAnimation extends EventEmitter<CameraAnimationEvent> {
* @param timing
*/
rotate(
operation: CameraRotate,
operation: ICameraRotate,
angle: number,
time: number,
start: number,
@ -568,7 +568,7 @@ export class CameraAnimation extends EventEmitter<CameraAnimationEvent> {
* @param timing
*/
scale(
operation: CameraScale,
operation: ICameraScale,
scale: number,
time: number,
start: number,

View File

@ -385,6 +385,7 @@ export abstract class GL2 extends RenderItem {
destroy(): void {
this.programs.forEach(v => v.destroy());
this.canvas.remove();
super.destroy();
}

View File

@ -462,6 +462,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
this.remove();
this.emit('destroy');
this.removeAllListeners();
this.cache.delete();
RenderItem.itemMap.delete(this._id);
}
}

View File

@ -1,5 +1,6 @@
import { EventEmitter } from '@/core/common/eventEmitter';
import { logger } from '@/core/common/logger';
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
interface BlockCacherEvent {
split: () => void;
@ -16,13 +17,22 @@ interface BlockData {
restHeight: number;
}
export interface IBlockCacheable {
/**
*
*/
destroy(): void;
}
/**
*
* 13x13划分缓存13x13的缓存分块
* 便`xx -> yy`
* xx说明传入的数据是元素还是分块的数据yy表示其返回值或转换为的值
*/
export class BlockCacher<T> extends EventEmitter<BlockCacherEvent> {
export class BlockCacher<
T extends IBlockCacheable
> extends EventEmitter<BlockCacherEvent> {
/** 区域宽度 */
width: number;
/** 区域高度 */
@ -118,7 +128,12 @@ export class BlockCacher<T> extends EventEmitter<BlockCacherEvent> {
const depth = this.cacheDepth;
for (let i = 0; i < depth; i++) {
if (deep & (1 << i)) {
this.cache.delete(index * this.cacheDepth + i);
const nowIndex = index * this.cacheDepth + i;
const item = this.cache.get(nowIndex);
if (item) {
item.destroy();
this.cache.delete(nowIndex);
}
}
}
}
@ -126,14 +141,19 @@ export class BlockCacher<T> extends EventEmitter<BlockCacherEvent> {
/**
* {@link clearCache} ->void
*/
clearCacheByIndex(index: number) {
this.cache.delete(index);
clearCacheByIndex(index: number, func: (item: T) => void) {
const item = this.cache.get(index);
if (item) {
item.destroy();
this.cache.delete(index);
}
}
/**
*
*/
clearAllCache() {
this.cache.forEach(v => v.destroy());
this.cache.clear();
}
@ -272,4 +292,24 @@ export class BlockCacher<T> extends EventEmitter<BlockCacherEvent> {
(y + 1) * this.blockSize
];
}
/**
*
*/
destroy() {
this.clearAllCache();
}
}
export interface ICanvasCacheItem extends IBlockCacheable {
readonly canvas: MotaOffscreenCanvas2D;
symbol: number;
}
export class CanvasCacheItem implements ICanvasCacheItem {
constructor(public canvas: MotaOffscreenCanvas2D, public symbol: number) {}
destroy(): void {
this.canvas.delete();
}
}

View File

@ -7,7 +7,12 @@ import {
LayerGroup
} from './layer';
import { ESpriteEvent, Sprite } from '../sprite';
import { BlockCacher } from './block';
import {
BlockCacher,
CanvasCacheItem,
IBlockCacheable,
ICanvasCacheItem
} from './block';
import type {
DamageEnemy,
EnemyCollection,
@ -16,7 +21,7 @@ import type {
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import { isNil } from 'lodash-es';
import { getDamageColor } from '@/plugin/utils';
import { transformCanvas } from '../item';
import { RenderItem, transformCanvas } from '../item';
import EventEmitter from 'eventemitter3';
import { Transform } from '../transform';
@ -120,11 +125,6 @@ export interface DamageRenderable {
strokeWidth?: number;
}
interface DamageCache {
canvas: MotaOffscreenCanvas2D;
symbol: number;
}
interface EDamageEvent extends ESpriteEvent {
setMapSize: [width: number, height: number];
beforeDamageRender: [need: Set<number>, transform: Transform];
@ -132,11 +132,11 @@ interface EDamageEvent extends ESpriteEvent {
dirtyUpdate: [block: number];
}
export class Damage extends Sprite<EDamageEvent> {
export class Damage extends RenderItem<EDamageEvent> {
mapWidth: number = 0;
mapHeight: number = 0;
block: BlockCacher<DamageCache>;
block: BlockCacher<ICanvasCacheItem>;
/** 键表示分块索引,值表示在这个分块上的渲染信息(当然实际渲染位置可以不在这个分块上) */
renderable: Map<number, Set<DamageRenderable>> = new Map();
@ -147,8 +147,6 @@ export class Damage extends Sprite<EDamageEvent> {
/** 单元格大小 */
cellSize: number = 32;
/** 伤害渲染层 */
damageMap: MotaOffscreenCanvas2D = new MotaOffscreenCanvas2D();
/** 默认伤害字体 */
font: string = '300 9px Verdana';
/** 默认描边样式,当伤害文字不存在描边属性时会使用此属性 */
@ -165,18 +163,15 @@ export class Damage extends Sprite<EDamageEvent> {
this.block = new BlockCacher(0, 0, core._WIDTH_, 1);
this.type = 'absolute';
this.size(core._PX_, core._PY_);
this.damageMap.withGameScale(true);
this.damageMap.setHD(true);
this.damageMap.setAntiAliasing(true);
this.damageMap.size(core._PX_, core._PY_);
this.setHD(true);
this.setAntiAliasing(true);
}
this.setRenderFn((canvas, transform) => {
const { ctx } = canvas;
const { width, height } = canvas;
ctx.imageSmoothingEnabled = false;
this.renderDamage(transform);
ctx.drawImage(this.damageMap.canvas, 0, 0, width, height);
});
protected render(
canvas: MotaOffscreenCanvas2D,
transform: Transform
): void {
this.renderDamage(canvas, transform);
}
private onExtract = () => {
@ -468,12 +463,10 @@ export class Damage extends Sprite<EDamageEvent> {
*
* @param transform
*/
renderDamage(transform: Transform) {
renderDamage(canvas: MotaOffscreenCanvas2D, transform: Transform) {
// console.time('damage');
const { ctx } = this.damageMap;
ctx.save();
this.damageMap.clear();
transformCanvas(this.damageMap, transform);
const { ctx } = canvas;
transformCanvas(canvas, transform);
// console.trace();
const render = this.calNeedRender(transform);
@ -529,10 +522,7 @@ export class Damage extends Sprite<EDamageEvent> {
});
ctx.drawImage(temp.canvas, px, py, size, size);
block.cache.set(v, {
canvas: temp,
symbol: temp.symbol
});
block.cache.set(v, new CanvasCacheItem(temp, temp.symbol));
});
ctx.restore();
// console.timeEnd('damage');
@ -540,6 +530,7 @@ export class Damage extends Sprite<EDamageEvent> {
destroy(): void {
super.destroy();
this.block.destroy();
this.enemy?.off('extract', this.onExtract);
}
}

View File

@ -5,7 +5,12 @@ import { TimingFn } from 'mutate-animate';
import { IAnimateFrame, renderEmits, RenderItem } from '../item';
import { logger } from '@/core/common/logger';
import { RenderableData, texture } from '../cache';
import { BlockCacher } from './block';
import {
BlockCacher,
CanvasCacheItem,
IBlockCacheable,
ICanvasCacheItem
} from './block';
import { Transform } from '../transform';
import { LayerFloorBinder, LayerGroupFloorBinder } from './floor';
import { RenderAdapter } from '../adapter';
@ -388,7 +393,7 @@ export interface ILayerRenderExtends {
* @param layer Layer实例
* @param images
*/
onBackgroundGenerated?(layer: Layer, images: HTMLCanvasElement[]): void;
onBackgroundGenerated?(layer: Layer, images: MotaOffscreenCanvas2D[]): void;
/**
* {@link Layer.putRenderData}
@ -504,11 +509,6 @@ export interface ILayerRenderExtends {
onDestroy?(layer: Layer): void;
}
interface LayerCacheItem {
symbol: number;
canvas: MotaOffscreenCanvas2D;
}
export interface LayerMovingRenderable extends RenderableData {
zIndex: number;
x: number;
@ -550,12 +550,17 @@ export class Layer extends Container {
/** 背景图块 */
background: AllNumbers = 0;
/** 背景图块画布 */
backImage: HTMLCanvasElement[] = [];
backImage: MotaOffscreenCanvas2D[] = [];
/** 背景贴图 */
floorImage: FloorAnimate[] = [];
/** 分块信息 */
block: BlockCacher<LayerCacheItem> = new BlockCacher(0, 0, core._WIDTH_, 4);
block: BlockCacher<ICanvasCacheItem> = new BlockCacher(
0,
0,
core._WIDTH_,
4
);
/** 大怪物渲染信息 */
bigImages: Map<number, LayerMovingRenderable> = new Map();
@ -717,13 +722,17 @@ export class Layer extends Container {
const num = this.background;
const data = texture.getRenderable(num);
this.backImage.forEach(v => v.delete());
this.backImage = [];
if (!data) return;
const frame = data.frame;
const temp = new MotaOffscreenCanvas2D();
temp.setHD(false);
temp.setAntiAliasing(false);
temp.withGameScale(false);
for (let i = 0; i < frame; i++) {
const canvas = new MotaOffscreenCanvas2D();
const temp = new MotaOffscreenCanvas2D();
const ctx = canvas.ctx;
const tempCtx = temp.ctx;
const [sx, sy, w, h] = data.render[i];
@ -731,9 +740,6 @@ export class Layer extends Container {
canvas.setAntiAliasing(false);
canvas.withGameScale(false);
canvas.size(core._PX_, core._PY_);
temp.setHD(false);
temp.setAntiAliasing(false);
temp.withGameScale(false);
temp.size(w, h);
const img = data.autotile ? data.image[0b11111111] : data.image;
@ -743,8 +749,9 @@ export class Layer extends Container {
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, canvas.width, canvas.height);
this.backImage.push(canvas.canvas);
this.backImage.push(canvas);
}
temp.delete();
for (const ex of this.extend.values()) {
ex.onBackgroundGenerated?.(this, this.backImage);
@ -1078,7 +1085,7 @@ export class Layer extends Container {
const sx = x * blockSize;
const sy = y * blockSize;
ctx.drawImage(
img,
img.canvas,
sx * cell,
sy * cell,
blockSize * cell,
@ -1180,10 +1187,7 @@ export class Layer extends Container {
blockSize * cell,
blockSize * cell
);
this.block.cache.set(index, {
canvas: temp,
symbol: temp.symbol
});
this.block.cache.set(index, new CanvasCacheItem(temp, temp.symbol));
});
}
@ -1379,6 +1383,12 @@ export class Layer extends Container {
ex.onDestroy?.(this);
}
super.destroy();
this.staticMap.delete();
this.movingMap.delete();
this.backMap.delete();
this.backImage.forEach(v => v.delete());
this.block.destroy();
this.main.destroy();
layerAdapter.remove(this);
}

View File

@ -78,7 +78,9 @@ export class MotaRenderer extends Container {
}
destroy() {
super.destroy();
MotaRenderer.list.delete(this.id);
this.target.delete();
}
static get(id: string) {

View File

@ -17,7 +17,7 @@ export function enableViewport() {
/**
*
*/
export function AddTiming(timing1: TimingFn, timing2: TimingFn): TimingFn {
export function addTiming(timing1: TimingFn, timing2: TimingFn): TimingFn {
return (p: number) => timing1(p) + timing2(p);
}

View File

@ -57,6 +57,7 @@
"30": "Cannot use indices named $1 since no definition for it. Please define it in advance.",
"31": "Cannot use indices since the indices instance is not belong to the program.",
"32": "Sub-image exceeds texture dimensions, auto adjusting size.",
"33": "Cannot modify MotaOffscreenCanvas2D that is freezed.",
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.",
"1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance."
}

View File

@ -2,9 +2,7 @@ import { Shader, ShaderProgram } from '@/core/render/shader';
import { IWeather, WeatherController } from './weather';
import { MotaRenderer } from '@/core/render/render';
import { Container } from '@/core/render/container';
import { GL2Program, IShaderUniform, UniformType } from '@/core/render/gl2';
import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d';
import { Transform } from '@/core/render/transform';
import { IShaderUniform, UniformType } from '@/core/render/gl2';
const rainVs = /* glsl */ `
in vec2 a_rainVertex;

View File

@ -163,8 +163,10 @@ export class ArrowProjectile extends Projectile<TowerBoss> {
this.easing = void 0;
this.dangerEasing = void 0;
this.horizontal?.clear();
this.horizontal?.delete();
this.horizontal = null;
this.vertical?.clear();
this.vertical?.delete();
this.vertical = null;
}
@ -465,6 +467,7 @@ export class ThunderProjectile extends Projectile<TowerBoss> {
static end() {
this.cache?.clear();
this.cache?.delete();
this.cache = null;
}
@ -648,8 +651,10 @@ export class ThunderBallProjectile extends Projectile<TowerBoss> {
static end() {
this.dangerEasing = void 0;
this.horizontal?.clear();
this.horizontal?.delete();
this.horizontal = null;
this.vertical?.clear();
this.vertical?.delete();
this.vertical = null;
}

View File

@ -337,6 +337,8 @@ export class Chase extends EventEmitter<ChaseEvent> {
Chase.shader.remove();
this.emit('end', success);
this.removeAllListeners();
this.pathMap.forEach(v => v.delete());
this.pathMap.clear();
}
}

View File

@ -1,7 +1,7 @@
import { Animation, hyper, linear, power, sleep } from 'mutate-animate';
import { Chase, ChaseData, IChaseController } from './chase';
import { completeAchievement } from '../ui/achievement';
import { Camera, CameraAnimation, CameraScale } from '@/core/render/camera';
import { Camera, CameraAnimation, ICameraScale } from '@/core/render/camera';
import { LayerGroup } from '@/core/render/preset/layer';
import { MotaRenderer } from '@/core/render/render';
import { Sprite } from '@/core/render/sprite';
@ -272,7 +272,7 @@ function playAudio(from: number, chase: Chase) {
function processScale(
chase: Chase,
ani: Animation,
scale: CameraScale,
scale: ICameraScale,
camera: Camera
) {
chase.onceLoc(35, 3, 'MT15', () => {