diff --git a/CHANGES b/CHANGES
index e7ae8a925..72a06e3b0 100644
--- a/CHANGES
+++ b/CHANGES
@@ -10,6 +10,7 @@ Emulation fixes:
- GBA Memory: Prevent writing to mirrored BG VRAM (fixes mgba.io/i/743)
- GBA Video: Fix sprite mosaic clamping (fixes mgba.io/i/1008)
- GB: Fix HALT when IE and IF unused bits are set (fixes mgba.io/i/1349)
+ - GBA Video: Implement mosaic on transformed sprites (fixes mgba.io/b/9)
Other fixes:
- Qt: More app metadata fixes
- Qt: Fix load recent from archive (fixes mgba.io/i/1325)
diff --git a/README.md b/README.md
index 501ff37f1..cf3cc29aa 100644
--- a/README.md
+++ b/README.md
@@ -215,7 +215,6 @@ Footnotes
[1] Currently missing features are
- OBJ window for modes 3, 4 and 5 ([Bug #5](http://mgba.io/b/5))
-- Mosaic for transformed OBJs ([Bug #9](http://mgba.io/b/9))
[2] Flash memory size detection does not work in some cases. These can be configured at runtime, but filing a bug is recommended if such a case is encountered.
diff --git a/README_DE.md b/README_DE.md
index f8a2a01d5..3f0b43ecf 100644
--- a/README_DE.md
+++ b/README_DE.md
@@ -215,7 +215,6 @@ Fußnoten
[1] Zurzeit fehlende Features sind
- OBJ-Fenster für die Modi 3, 4 und 5 ([Bug #5](http://mgba.io/b/5))
-- Mosaik-Effekt für umgewandelte OBJs ([Bug #9](http://mgba.io/b/9))
[2] In manchen Fällen ist es nicht möglich, die Größe des Flash-Speichers automatisch zu ermitteln. Diese kann dann zur Laufzeit konfiguriert werden, es wird jedoch empfohlen, den Fehler zu melden.
diff --git a/src/gba/renderers/software-obj.c b/src/gba/renderers/software-obj.c
index 8d6ea3768..bd8a29a71 100644
--- a/src/gba/renderers/software-obj.c
+++ b/src/gba/renderers/software-obj.c
@@ -48,6 +48,31 @@
SPRITE_DRAW_PIXEL_ ## DEPTH ## _ ## TYPE(localX); \
}
+#define SPRITE_TRANSFORMED_MOSAIC_LOOP(DEPTH, TYPE) \
+ unsigned tileData; \
+ unsigned widthMask = ~(width - 1); \
+ unsigned heightMask = ~(height - 1); \
+ int localX = xAccum >> 8; \
+ int localY = yAccum >> 8; \
+ for (; outX < condition; ++outX, ++inX) { \
+ renderer->spriteCyclesRemaining -= 2; \
+ xAccum += mat.a; \
+ yAccum += mat.c; \
+ \
+ if (outX % mosaicH == 0) { \
+ localX = xAccum >> 8; \
+ localY = yAccum >> 8; \
+ } \
+ \
+ if (localX & widthMask || localY & heightMask) { \
+ continue; \
+ } \
+ \
+ SPRITE_YBASE_ ## DEPTH(localY); \
+ SPRITE_XBASE_ ## DEPTH(localX); \
+ SPRITE_DRAW_PIXEL_ ## DEPTH ## _ ## TYPE(localX); \
+ }
+
#define SPRITE_XBASE_16(localX) unsigned xBase = (localX & ~0x7) * 4 + ((localX >> 1) & 2);
#define SPRITE_YBASE_16(localY) unsigned yBase = (localY & ~0x7) * stride + (localY & 0x7) * 4;
@@ -173,6 +198,13 @@ int GBAVideoSoftwareRendererPreprocessSprite(struct GBAVideoSoftwareRenderer* re
if (end < condition) {
condition = end;
}
+ int mosaicH = 1;
+ if (GBAObjAttributesAIsMosaic(sprite->a)) {
+ mosaicH = GBAMosaicControlGetObjH(renderer->mosaic) + 1;
+ if (condition % mosaicH) {
+ condition += mosaicH - (condition % mosaicH);
+ }
+ }
int xAccum = mat.a * (inX - 1 - (totalWidth >> 1)) + mat.b * (inY - (totalHeight >> 1)) + (width << 7);
int yAccum = mat.c * (inX - 1 - (totalWidth >> 1)) + mat.d * (inY - (totalHeight >> 1)) + (height << 7);
@@ -223,6 +255,13 @@ int GBAVideoSoftwareRendererPreprocessSprite(struct GBAVideoSoftwareRenderer* re
palette = &palette[GBAObjAttributesCGetPalette(sprite->c) << 4];
if (flags & FLAG_OBJWIN) {
SPRITE_TRANSFORMED_LOOP(16, OBJWIN);
+ } else if (mosaicH > 1) {
+ if (objwinSlowPath) {
+ objwinPalette = &objwinPalette[GBAObjAttributesCGetPalette(sprite->c) << 4];
+ SPRITE_TRANSFORMED_MOSAIC_LOOP(16, NORMAL_OBJWIN);
+ } else {
+ SPRITE_TRANSFORMED_MOSAIC_LOOP(16, NORMAL);
+ }
} else if (objwinSlowPath) {
objwinPalette = &objwinPalette[GBAObjAttributesCGetPalette(sprite->c) << 4];
SPRITE_TRANSFORMED_LOOP(16, NORMAL_OBJWIN);
@@ -232,6 +271,12 @@ int GBAVideoSoftwareRendererPreprocessSprite(struct GBAVideoSoftwareRenderer* re
} else {
if (flags & FLAG_OBJWIN) {
SPRITE_TRANSFORMED_LOOP(256, OBJWIN);
+ } else if (mosaicH > 1) {
+ if (objwinSlowPath) {
+ SPRITE_TRANSFORMED_MOSAIC_LOOP(256, NORMAL_OBJWIN);
+ } else {
+ SPRITE_TRANSFORMED_MOSAIC_LOOP(256, NORMAL);
+ }
} else if (objwinSlowPath) {
SPRITE_TRANSFORMED_LOOP(256, NORMAL_OBJWIN);
} else {