1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
5  * file distributed with this source distribution.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22 
23 //=============================================================================
24 //
25 // Software drawing component. Optimizes drawing for software renderer using
26 // dirty rectangles technique.
27 //
28 // TODO: do research/profiling to find out if this dirty rectangles thing
29 // is still giving ANY notable perfomance boost at all.
30 //
31 // TODO: would that give any benefit to reorganize the code and move dirty
32 // rectangles into SoftwareGraphicDriver?
33 // Alternatively: we could pass dirty rects struct pointer and room background
34 // DDB when calling BeginSpriteBatch(). Driver itself could be calling
35 // update_invalid_region(). That will keep gfx driver's changes to minimum.
36 //
37 // NOTE: this code, including structs and functions, has underwent several
38 // iterations of changes. Originally it was meant to perform full transform
39 // of dirty rects right away, but later I realized it won't work that way
40 // because a) Allegro does not support scaling bitmaps over destination with
41 // different colour depth (which may be a case when running 16-bit game),
42 // and b) Allegro does not support scaling and rotating of sprites with
43 // blending and lighting at the same time which means that room objects have
44 // to be drawn upon non-scaled background first. Possibly some of the code
45 // below may be therefore simplified.
46 //
47 //=============================================================================
48 
49 #include "ags/lib/std/vector.h"
50 #include "ags/engine/ac/draw_software.h"
51 #include "ags/shared/gfx/bitmap.h"
52 #include "ags/shared/util/scaling.h"
53 #include "ags/globals.h"
54 
55 namespace AGS3 {
56 
57 using namespace AGS::Shared;
58 using namespace AGS::Engine;
59 
IRSpan()60 IRSpan::IRSpan()
61 	: x1(0), x2(0) {
62 }
63 
IRRow()64 IRRow::IRRow()
65 	: numSpans(0) {
66 }
67 
mergeSpan(int tx1,int tx2)68 int IRSpan::mergeSpan(int tx1, int tx2) {
69 	if ((tx1 > x2) || (tx2 < x1))
70 		return 0;
71 	// overlapping, increase the span
72 	if (tx1 < x1)
73 		x1 = tx1;
74 	if (tx2 > x2)
75 		x2 = tx2;
76 	return 1;
77 }
78 
DirtyRects()79 DirtyRects::DirtyRects()
80 	: NumDirtyRegions(0) {
81 }
82 
IsInit() const83 bool DirtyRects::IsInit() const {
84 	return DirtyRows.size() > 0;
85 }
86 
Init(const Size & surf_size,const Rect & viewport)87 void DirtyRects::Init(const Size &surf_size, const Rect &viewport) {
88 	int height = surf_size.Height;
89 	if (SurfaceSize != surf_size) {
90 		Destroy();
91 		SurfaceSize = surf_size;
92 		DirtyRows.resize(height);
93 
94 		NumDirtyRegions = WHOLESCREENDIRTY;
95 		for (int i = 0; i < height; ++i)
96 			DirtyRows[i].numSpans = 0;
97 	}
98 
99 	Viewport = viewport;
100 	Room2Screen.Init(surf_size, viewport);
101 	Screen2DirtySurf.Init(viewport, RectWH(0, 0, surf_size.Width, surf_size.Height));
102 }
103 
SetSurfaceOffsets(int x,int y)104 void DirtyRects::SetSurfaceOffsets(int x, int y) {
105 	Room2Screen.SetSrcOffsets(x, y);
106 }
107 
Destroy()108 void DirtyRects::Destroy() {
109 	DirtyRows.clear();
110 	NumDirtyRegions = 0;
111 }
112 
Reset()113 void DirtyRects::Reset() {
114 	NumDirtyRegions = 0;
115 
116 	for (size_t i = 0; i < DirtyRows.size(); ++i)
117 		DirtyRows[i].numSpans = 0;
118 }
119 
dispose_invalid_regions(bool)120 void dispose_invalid_regions(bool /* room_only */) {
121 	_GP(RoomCamRects).clear();
122 	_GP(RoomCamPositions).clear();
123 }
124 
set_invalidrects_globaloffs(int x,int y)125 void set_invalidrects_globaloffs(int x, int y) {
126 	_GP(GlobalOffs) = Point(x, y);
127 }
128 
init_invalid_regions(int view_index,const Size & surf_size,const Rect & viewport)129 void init_invalid_regions(int view_index, const Size &surf_size, const Rect &viewport) {
130 	if (view_index < 0) {
131 		_GP(BlackRects).Init(surf_size, viewport);
132 	} else {
133 		if (_GP(RoomCamRects).size() <= (size_t)view_index) {
134 			_GP(RoomCamRects).resize(view_index + 1);
135 			_GP(RoomCamPositions).resize(view_index + 1);
136 		}
137 		_GP(RoomCamRects)[view_index].Init(surf_size, viewport);
138 		_GP(RoomCamPositions)[view_index] = std::make_pair(-1000, -1000);
139 	}
140 }
141 
delete_invalid_regions(int view_index)142 void delete_invalid_regions(int view_index) {
143 	if (view_index >= 0) {
144 		_GP(RoomCamRects).erase(_GP(RoomCamRects).begin() + view_index);
145 		_GP(RoomCamPositions).erase(_GP(RoomCamPositions).begin() + view_index);
146 	}
147 }
148 
set_invalidrects_cameraoffs(int view_index,int x,int y)149 void set_invalidrects_cameraoffs(int view_index, int x, int y) {
150 	if (view_index < 0) {
151 		_GP(BlackRects).SetSurfaceOffsets(x, y);
152 		return;
153 	} else {
154 		_GP(RoomCamRects)[view_index].SetSurfaceOffsets(x, y);
155 	}
156 
157 	int &posxwas = _GP(RoomCamPositions)[view_index].first;
158 	int &posywas = _GP(RoomCamPositions)[view_index].second;
159 	if ((x != posxwas) || (y != posywas)) {
160 		invalidate_all_camera_rects(view_index);
161 		posxwas = x;
162 		posywas = y;
163 	}
164 }
165 
invalidate_all_rects()166 void invalidate_all_rects() {
167 	for (auto &rects : _GP(RoomCamRects)) {
168 		if (!IsRectInsideRect(rects.Viewport, _GP(BlackRects).Viewport))
169 			_GP(BlackRects).NumDirtyRegions = WHOLESCREENDIRTY;
170 		rects.NumDirtyRegions = WHOLESCREENDIRTY;
171 	}
172 }
173 
invalidate_all_camera_rects(int view_index)174 void invalidate_all_camera_rects(int view_index) {
175 	if (view_index < 0)
176 		return;
177 	_GP(RoomCamRects)[view_index].NumDirtyRegions = WHOLESCREENDIRTY;
178 }
179 
invalidate_rect_on_surf(int x1,int y1,int x2,int y2,DirtyRects & rects)180 void invalidate_rect_on_surf(int x1, int y1, int x2, int y2, DirtyRects &rects) {
181 	if (rects.DirtyRows.size() == 0)
182 		return;
183 	if (rects.NumDirtyRegions >= MAXDIRTYREGIONS) {
184 		// too many invalid rectangles, just mark the whole thing dirty
185 		rects.NumDirtyRegions = WHOLESCREENDIRTY;
186 		return;
187 	}
188 
189 	int a;
190 
191 	const Size &surfsz = rects.SurfaceSize;
192 	if (x1 >= surfsz.Width) x1 = surfsz.Width - 1;
193 	if (y1 >= surfsz.Height) y1 = surfsz.Height - 1;
194 	if (x2 >= surfsz.Width) x2 = surfsz.Width - 1;
195 	if (y2 >= surfsz.Height) y2 = surfsz.Height - 1;
196 	if (x1 < 0) x1 = 0;
197 	if (y1 < 0) y1 = 0;
198 	if (x2 < 0) x2 = 0;
199 	if (y2 < 0) y2 = 0;
200 	rects.NumDirtyRegions++;
201 
202 	// ** Span code
203 	std::vector<IRRow> &dirtyRow = rects.DirtyRows;
204 	int s, foundOne;
205 	// add this rect to the list for this row
206 	for (a = y1; a <= y2; a++) {
207 		foundOne = 0;
208 		for (s = 0; s < dirtyRow[a].numSpans; s++) {
209 			if (dirtyRow[a].span[s].mergeSpan(x1, x2)) {
210 				foundOne = 1;
211 				break;
212 			}
213 		}
214 		if (foundOne) {
215 			// we were merged into a span, so we're ok
216 			int t;
217 			// check whether now two of the spans overlap each other
218 			// in which case merge them
219 			for (s = 0; s < dirtyRow[a].numSpans; s++) {
220 				for (t = s + 1; t < dirtyRow[a].numSpans; t++) {
221 					if (dirtyRow[a].span[s].mergeSpan(dirtyRow[a].span[t].x1, dirtyRow[a].span[t].x2)) {
222 						dirtyRow[a].numSpans--;
223 						for (int u = t; u < dirtyRow[a].numSpans; u++)
224 							dirtyRow[a].span[u] = dirtyRow[a].span[u + 1];
225 						break;
226 					}
227 				}
228 			}
229 		} else if (dirtyRow[a].numSpans < MAX_SPANS_PER_ROW) {
230 			dirtyRow[a].span[dirtyRow[a].numSpans].x1 = x1;
231 			dirtyRow[a].span[dirtyRow[a].numSpans].x2 = x2;
232 			dirtyRow[a].numSpans++;
233 		} else {
234 			// didn't fit in an existing span, and there are none spare
235 			int nearestDist = 99999, nearestWas = -1, extendLeft = false;
236 			int tleft, tright;
237 			// find the nearest span, and enlarge that to include this rect
238 			for (s = 0; s < dirtyRow[a].numSpans; s++) {
239 				tleft = dirtyRow[a].span[s].x1 - x2;
240 				if ((tleft > 0) && (tleft < nearestDist)) {
241 					nearestDist = tleft;
242 					nearestWas = s;
243 					extendLeft = 1;
244 				}
245 				tright = x1 - dirtyRow[a].span[s].x2;
246 				if ((tright > 0) && (tright < nearestDist)) {
247 					nearestDist = tright;
248 					nearestWas = s;
249 					extendLeft = 0;
250 				}
251 			}
252 			if (extendLeft)
253 				dirtyRow[a].span[nearestWas].x1 = x1;
254 			else
255 				dirtyRow[a].span[nearestWas].x2 = x2;
256 		}
257 	}
258 	// ** End span code
259 	//}
260 }
261 
invalidate_rect_ds(DirtyRects & rects,int x1,int y1,int x2,int y2,bool in_room)262 void invalidate_rect_ds(DirtyRects &rects, int x1, int y1, int x2, int y2, bool in_room) {
263 	if (!in_room) {
264 		// TODO: for most opimisation (esp. with multiple viewports) should perhaps
265 		// split/cut parts of the original rectangle which overlap room viewport(s).
266 		Rect r(x1, y1, x2, y2);
267 		// If overlay is NOT completely over the room, then invalidate black rect
268 		if (!IsRectInsideRect(rects.Viewport, r))
269 			invalidate_rect_on_surf(x1, y1, x2, y2, _GP(BlackRects));
270 		// If overlay is NOT intersecting room viewport at all, then stop
271 		if (!AreRectsIntersecting(rects.Viewport, r))
272 			return;
273 
274 		// Transform from screen to room coordinates through the known viewport
275 		x1 = rects.Screen2DirtySurf.X.ScalePt(x1);
276 		x2 = rects.Screen2DirtySurf.X.ScalePt(x2);
277 		y1 = rects.Screen2DirtySurf.Y.ScalePt(y1);
278 		y2 = rects.Screen2DirtySurf.Y.ScalePt(y2);
279 	} else {
280 		// Transform only from camera pos to room background
281 		x1 -= rects.Room2Screen.X.GetSrcOffset();
282 		y1 -= rects.Room2Screen.Y.GetSrcOffset();
283 		x2 -= rects.Room2Screen.X.GetSrcOffset();
284 		y2 -= rects.Room2Screen.Y.GetSrcOffset();
285 	}
286 
287 	invalidate_rect_on_surf(x1, y1, x2, y2, rects);
288 }
289 
invalidate_rect_ds(int x1,int y1,int x2,int y2,bool in_room)290 void invalidate_rect_ds(int x1, int y1, int x2, int y2, bool in_room) {
291 	if (!in_room) { // convert from game viewport to global screen coords
292 		x1 += _GP(GlobalOffs).X;
293 		x2 += _GP(GlobalOffs).X;
294 		y1 += _GP(GlobalOffs).Y;
295 		y2 += _GP(GlobalOffs).Y;
296 	}
297 
298 	for (auto &rects : _GP(RoomCamRects))
299 		invalidate_rect_ds(rects, x1, y1, x2, y2, in_room);
300 }
301 
invalidate_rect_global(int x1,int y1,int x2,int y2)302 void invalidate_rect_global(int x1, int y1, int x2, int y2) {
303 	for (auto &rects : _GP(RoomCamRects))
304 		invalidate_rect_ds(rects, x1, y1, x2, y2, false);
305 }
306 
307 // Note that this function is denied to perform any kind of scaling or other transformation
308 // other than blitting with offset. This is mainly because destination could be a 32-bit virtual screen
309 // while room background was 16-bit and Allegro lib does not support stretching between colour depths.
310 // The no_transform flag here means essentially "no offset", and indicates that the function
311 // must blit src on ds at 0;0. Otherwise, actual Viewport offset is used.
update_invalid_region(Bitmap * ds,Bitmap * src,const DirtyRects & rects,bool no_transform)312 void update_invalid_region(Bitmap *ds, Bitmap *src, const DirtyRects &rects, bool no_transform) {
313 	if (rects.NumDirtyRegions == 0)
314 		return;
315 
316 	if (!no_transform)
317 		ds->SetClip(rects.Viewport);
318 
319 	const int src_x = rects.Room2Screen.X.GetSrcOffset();
320 	const int src_y = rects.Room2Screen.Y.GetSrcOffset();
321 	const int dst_x = no_transform ? 0 : rects.Viewport.Left;
322 	const int dst_y = no_transform ? 0 : rects.Viewport.Top;
323 
324 	if (rects.NumDirtyRegions == WHOLESCREENDIRTY) {
325 		ds->Blit(src, src_x, src_y, dst_x, dst_y, rects.SurfaceSize.Width, rects.SurfaceSize.Height);
326 	} else {
327 		const std::vector<IRRow> &dirtyRow = rects.DirtyRows;
328 		const int surf_height = rects.SurfaceSize.Height;
329 		// TODO: is this IsMemoryBitmap check is still relevant?
330 		// If bitmaps properties match and no transform required other than linear offset
331 		if (src->GetColorDepth() == ds->GetColorDepth()) {
332 			const int bypp = src->GetBPP();
333 			// do the fast memory copy
334 			for (int i = 0; i < surf_height; i++) {
335 				const uint8_t *src_scanline = src->GetScanLine(i + src_y);
336 				uint8_t *dst_scanline = ds->GetScanLineForWriting(i + dst_y);
337 				const IRRow &dirty_row = dirtyRow[i];
338 				for (int k = 0; k < dirty_row.numSpans; k++) {
339 					int tx1 = dirty_row.span[k].x1;
340 					int tx2 = dirty_row.span[k].x2;
341 					memcpy(&dst_scanline[(tx1 + dst_x) * bypp], &src_scanline[(tx1 + src_x) * bypp], ((tx2 - tx1) + 1) * bypp);
342 				}
343 			}
344 		}
345 		// If has to use Blit, but still must draw with no transform but offset
346 		else {
347 			// do fast copy without transform
348 			for (int i = 0, rowsInOne = 1; i < surf_height; i += rowsInOne, rowsInOne = 1) {
349 				// if there are rows with identical masks, do them all in one go
350 				// TODO: what is this for? may this be done at the invalidate_rect merge step?
351 				while ((i + rowsInOne < surf_height) && (memcmp(&dirtyRow[i], &dirtyRow[i + rowsInOne], sizeof(IRRow)) == 0))
352 					rowsInOne++;
353 
354 				const IRRow &dirty_row = dirtyRow[i];
355 				for (int k = 0; k < dirty_row.numSpans; k++) {
356 					int tx1 = dirty_row.span[k].x1;
357 					int tx2 = dirty_row.span[k].x2;
358 					ds->Blit(src, tx1 + src_x, i + src_y, tx1 + dst_x, i + dst_y, (tx2 - tx1) + 1, rowsInOne);
359 				}
360 			}
361 		}
362 	}
363 }
364 
update_invalid_region(Bitmap * ds,color_t fill_color,const DirtyRects & rects)365 void update_invalid_region(Bitmap *ds, color_t fill_color, const DirtyRects &rects) {
366 	ds->SetClip(rects.Viewport);
367 
368 	if (rects.NumDirtyRegions == WHOLESCREENDIRTY) {
369 		ds->FillRect(rects.Viewport, fill_color);
370 	} else {
371 		const std::vector<IRRow> &dirtyRow = rects.DirtyRows;
372 		const int surf_height = rects.SurfaceSize.Height;
373 		{
374 			const AGS::Shared::PlaneScaling &tf = rects.Room2Screen;
375 			for (int i = 0, rowsInOne = 1; i < surf_height; i += rowsInOne, rowsInOne = 1) {
376 				// if there are rows with identical masks, do them all in one go
377 				// TODO: what is this for? may this be done at the invalidate_rect merge step?
378 				while ((i + rowsInOne < surf_height) && (memcmp(&dirtyRow[i], &dirtyRow[i + rowsInOne], sizeof(IRRow)) == 0))
379 					rowsInOne++;
380 
381 				const IRRow &dirty_row = dirtyRow[i];
382 				for (int k = 0; k < dirty_row.numSpans; k++) {
383 					Rect src_r(dirty_row.span[k].x1, i, dirty_row.span[k].x2, i + rowsInOne - 1);
384 					Rect dst_r = tf.ScaleRange(src_r);
385 					ds->FillRect(dst_r, fill_color);
386 				}
387 			}
388 		}
389 	}
390 }
391 
update_black_invreg_and_reset(Bitmap * ds)392 void update_black_invreg_and_reset(Bitmap *ds) {
393 	if (!_GP(BlackRects).IsInit())
394 		return;
395 	update_invalid_region(ds, (color_t)0, _GP(BlackRects));
396 	_GP(BlackRects).Reset();
397 }
398 
update_room_invreg_and_reset(int view_index,Bitmap * ds,Bitmap * src,bool no_transform)399 void update_room_invreg_and_reset(int view_index, Bitmap *ds, Bitmap *src, bool no_transform) {
400 	if (view_index < 0 || _GP(RoomCamRects).size() == 0)
401 		return;
402 
403 	update_invalid_region(ds, src, _GP(RoomCamRects)[view_index], no_transform);
404 	_GP(RoomCamRects)[view_index].Reset();
405 }
406 
407 } // namespace AGS3
408