sprite renderer

tile の場合は 1chip 分, 8dot 一気にラインバッファへ書き込みが出来るのだが、sprite の場合は 1dot 単位で attribute の color とキャラを書く必要がある。

1dot ずつちまちま描画を行っても良いのだが、時間がかかるので1度に 2dot 書き込みを行う実装にした。 4dot, 8dot もやろうと思えばできるのだろうけど、複雑になってわけがわからなくなったり、時間が足りるのでこのまま。

今回は ZOOM がないので、次のdotが3dot先だったり(zoom down)、同じだったり(zoom up)ということがなく、次のdotは1dot隣で助かる。今までは chip 内部 offset で 32bit のうちどこから 4bit を取るかという switch/case を組んでいた。今回は MSB 4bit が描画 bit という風にシフトレジスタにした。なんでいままでシフトレジスタにしなかったんだろう。

lb_x0_d = {color, lb0_d};
lb_x0_latch = if(address_switch == 0) 1 else lb0_d != 4'h0;
lb_x1_d = {color, lb1_d};
lb_x1_latch = if(address_switch == 0) 1 else lb1_d != 4'h0;

lb_x0 は x 座標の bit0 が 0 向けのラインバッファ, lb_x1 は bit0 が 1 のものでこの2つを持つ。 d が 0 の時は 0 なので透過とし、書き込みをしない。

reg pattern[28];

pattern はキャラクタROMから来るデータをラッチするもので、7bit分もつ。[27:24] が lb0_d, [23:20] が lb1_d に書き込まれる。

state pattern_load{
	if(vram_ready == 1'b0){
		vram_read := 1;
		if(x_axis[0] == 1'b0 && x_flip == 1'b0){
			lb0_d := vram_d[31:28];
			lb1_d := vram_d[27:24];
			pattern := {vram_d[23:0], 4'h0};
		}else if(x_axis[0] == 1'b0 && x_flip == 1'b1){
			lb0_d := vram_d[3:0];
			lb1_d := vram_d[7:4];
			pattern := {vram_d[11:8], vram_d[15:12], vram_d[19:16], 
				vram_d[23:20], vram_d[27:24], vram_d[31:28], 4'h0};
		}else if(x_axis[0] == 1'b1 && x_flip == 1'b0){
			lb0_d := pattern[27:24];
			lb1_d := vram_d[31:28];
			
			pattern := vram_d[27:0];
		}if(x_axis[0] == 1'b1 && x_flip == 1'b1){
			lb0_d := pattern[27:24];
			lb1_d := vram_d[3:0];

			pattern := {vram_d[7:4], vram_d[11:8], vram_d[15:12], 
				vram_d[19:16], vram_d[23:20], vram_d[27:24], vram_d[31:28]};
		}else{
			pattern := 28'hx;
		}
		goto render_next;
	}
}

この state はキャラクタ ROM からデータが来るまで待ち、データが来た直後にラインバッファへの書き込みと pattern へのラッチを行う。x 座標が偶数のときは ROM からのデータ2つを書き込むが、x 座標が奇数のときは lb1 に左端のキャラクタをいれる。 lb0 は pattern[27:24] となっているが、 0 が入っているのでラッチされない。

state render_next{
	if(render_end == 1'b0){
		vram_select := 1;
		vram_read := 1;
		_display("line %d: sprite render out", dot_y);
		goto init;
		finish();
	}else if(render_last){
		if(x_axis[0] == 1){
			lb0_d := pattern[27:24];
		}else{
			lb0_d := 4'h0;
		}
		lb1_d := 4'h0;
		offset_x := offset_x + 2;
		if(attribute_a != 7'h7f){
			attribute_a := attribute_a + 7'd1;
			goto attr_wait;
		}else{
			vram_select := 1;
			goto init;
			finish();
		}
	}else if(render_load){
		lb0_d := 4'h0;
		lb1_d := 4'h0;
		offset_x := offset_x + 2;

		vram_read := 0;
		goto pattern_wait;
	}else{
		lb0_d := pattern[27:24];
		lb1_d := pattern[23:20];

		pattern := pattern << 8;
		offset_x := offset_x + 2;
		goto render_next;
	}
}

render_end はスプライト描き込みの時間切れの処理。以前に作ったものは state の最上位にいれていたものの、NSL の言語仕様でそういうことは簡単にできなくなってしまったので、ここにいれた。

render_last は描画スプライトの右端まで達したときの分岐。 x 座標が偶数の場合は意味がないが、奇数の場合は最後の1dotを描き込む。lb1 は使用しない。

render_load は次の 8bit のキャラクタを読み込み時に使用する。この処理は速めにやっておけばキャラクタ ROM のデータ待ちの時間を減らせそう。

最後の else は通常の書き込みで pattern をシフトする。ここでは x 座標の偶数奇数を気にしなくて良い。

という感じ。NSL のおかげで非常に簡潔に書けた。ありがとう NSL 。