Revision 212 (by dpavlin, 2008/04/14 21:26:32) fix check if arg is array
package Screen;

# Dobrica Pavlinusic, <dpavlin@rot13.org> 07/30/07 17:58:55 CEST

use strict;
use warnings;

use SDL::App;
use SDL::Rect;
use SDL::Color;
use SDL::Constants;

use Carp qw/confess/;
use Data::Dump qw/dump/;

use Exporter 'import';
our @EXPORT = qw'$white $black @flip';

use base qw(Class::Accessor Prefs);
__PACKAGE__->mk_accessors(qw(app event screen_width screen_height window_width window_height));

=head1 NAME

Screen - simulated monochrome screen using SDL

=head1 Architecture dependent

You may override following methods if you want to implement keyboard on each
keypress event. Alternative is to use <read> hook and trap memory access.

=head2 screen_width

Width of emulated screen (256 by default)

=head2 screen_height

Height of emulated screen (256 by default)

=head2 key_down

  $self->key_down( 'a' );

=cut

sub key_down {}

=head2 key_up

  $self->key_up( 'a' );

=cut

sub key_up {}


=head1 Architecture independent

You don't need to override any of following function in your architecture,
but you might want to call them.

=head2 open_screen

Open simulated screen

=cut

our $app;

sub open_screen {
	my $self = shift;

	$self->prefs;

	if ( ! $self->scale ) {
		$self->scale( 1 );
		warn "using default unscaled display\n";
	}

	$self->screen_width( 256 ) unless defined $self->screen_width;
	$self->screen_height( 256 ) unless defined $self->screen_height;

	my $w = $self->screen_width * $self->scale + ( $self->show_mem ? 256 : 0 );
	$self->window_width( $w );

	my $h = $self->screen_height;
	# expand screen size to show whole 64k 256*256 memory map
	$h = 256 if $self->show_mem && $h < 256;
	$h *= $self->scale;
	$self->window_height( $h );

	$app = SDL::App->new(
		-width  => $w,
		-height => $h,
		-depth  => 16,
		-flags=>SDL_DOUBLEBUF | SDL_HWSURFACE | SDL_HWACCEL,
	);
	#$app->grab_input( SDL_GRAB_QUERY );
	$app->grab_input( SDL_GRAB_OFF );
	$app->title( ref($self) );

	$self->app( $app );

	my $event = SDL::Event->new();
	$self->event( $event );

	warn "# created SDL::App with screen ", $self->screen_width, "x", $self->screen_height, " in window ",
		$self->window_width, "x", $self->window_height, "\n";
}

our $white	= SDL::Color->new( -r => 0xff, -g => 0xff, -b => 0xff );
our $black	= SDL::Color->new( -r => 0x80, -g => 0x80, -b => 0x80 );

my $red		= SDL::Color->new( -r => 0xff, -g => 0x00, -b => 0x00 );
my $green	= SDL::Color->new( -r => 0x00, -g => 0xff, -b => 0x00 );
my $blue	= SDL::Color->new( -r => 0x00, -g => 0x00, -b => 0xff );

=head2 mem_xy

Helper to return x and y coordinates in memory map

  my ( $x,$y ) = $screen->mem_xy( $address );

=cut

sub mem_xy {
	my $self = shift;
	my $offset = shift;
	my $x = $offset & 0xff;
	$x += $self->screen_width * $self->scale;
	my $y = $offset >> 8;
	return ($x,$y);
}

=head2 mmap_pixel

Draw pixel in memory map

  $self->mmap_pixel( $addr, $r, $g, $b );

=cut

# keep accesses to memory
my $_mem_stat;

sub mmap_pixel {
	my ( $self, $addr, $r, $g, $b ) = @_;
	return unless $self->show_mem && $self->app;

	my ( $x, $y ) = $self->mem_xy( $addr );
	warn sprintf "## mem %04x %02x %02x %02d*%02d\n", $addr, $r, $g, $x, $y if $self->debug;

	my $col = SDL::Color->new( -r => $r, -g => $g, -b => $b );
	$self->app->pixel( $x, $y, $col );

	$_mem_stat++;
	if ( $_mem_stat % 1000 == 0 ) {
		$self->app->sync;
	}
}


=head2 sync

  $self->sync;

=cut

sub sync {
	$app->sync;
}

=head2 render_vram

Render one frame of video ram

  $self->render_vram;

=cut

sub render_vram {
	my $self = shift;

	confess "please implement $self::render_vram";
}


=head2 render_frame

Render one frame of video ram

  $self->render_frame( $vram_sdl_surface );

=cut

sub render_frame {
	my $self = shift;

	my $vram = shift;
	confess "need SDL::Surface as argument" unless ref($vram) eq 'SDL::Surface';

	$vram->display_format;

	my $scale = $self->scale || confess "no scale?";

	my $rect        = SDL::Rect->new( -x => 0, -y => 0, -width => $self->screen_width * $scale, -height => $self->screen_height * $scale );
	my $rect_screen = SDL::Rect->new( -x => 0, -y => 0, -width => $self->screen_width * $scale, -height => $self->screen_height * $scale );

	if ( $scale > 1 ) {
		use SDL::Tool::Graphic;
		# last parametar is anti-alias
		my $zoomed = SDL::Tool::Graphic->zoom( $vram, $self->scale, $self->scale, 1 );
		$zoomed->blit( $rect, $app, $rect_screen );
	} else {
		$vram->blit( $rect, $app, $rect_screen );
	}

	$app->sync;
}


=head2 render_mem

  $self->render_mem( @mem );
  $self->render_mem( $memory_bytes );

=cut

sub render_mem {
	my $self = shift;

	return unless $self->show_mem;

	my $pixels;
	if ( defined $# ) {
		$pixels = pack("C*", @_);
	} else {
		$pixels = shift;
	}

	my $vram = SDL::Surface->new(
		-width => 256,
		-height => 256,
		-depth => 8,	# 1 bit per pixel
		-pitch => 256,	# bytes per line
		-from => $pixels,
		-Rmask => 0xffff00ff,
		-Gmask => 0xffff00ff,
		-Bmask => 0xffff00ff,
	);

	$vram->display_format;

	my $rect     = SDL::Rect->new( -x => 0, -y => 0, -width => $self->screen_width, -height => $self->window_height );
	my $rect_mem = SDL::Rect->new( -x => $self->screen_width * $self->scale, -y => 0, -width => 256, -height => 256 );

	$vram->blit( $rect, $app, $rect_mem );

	$app->sync;
}

=head2 key_pressed

Check SDL event loop if there are any pending keys

  my $key = $self->key_pressed;

  if ( $self->key_pressed( 1 ) ) {
  	# just to check other events, don't process
	# key
  }

=cut

my $pending_key;
my $key_active;
my $run_for = 2000;

sub key_pressed {
	my $self = shift;

	# don't take key, just pull event
	my $just_checking = shift || 0;

	my $event = $self->event || confess "no event?";

	if ( ! $event->poll ) {
		return $pending_key unless $self->can('session_event');
		if ( my $h = $self->session_event('key_pressed') ) {
			my ( $key, $state ) = %$h;
			if ( $state ) {
				$pending_key = $key;
				$self->key_down( $key );
				$key_active->{$key} = 1;
			} else {
				undef $pending_key;
				$self->key_up( $key );
				$key_active->{$key} = 0;
			}
		}
		return $pending_key;
	}

	my $type = $event->type();

	exit if ($type == SDL_QUIT);

	my $k = $pending_key;

	if ($type == SDL_KEYDOWN) {
		$k = $event->key_name();
		if ( $k eq 'escape' ) {
			$run_for = $self->cli;
			warn "will check event loop every $run_for cycles\n";
			$pending_key = '~';
		} else {
			warn "SDL_KEYDOWN ($type) = '$k'", $just_checking ? ' fake' : '', "\n";
			$pending_key = $k;
			$self->key_down( $k );
			$key_active->{$k} = 1;
			$self->record_session('key_pressed', { $k => 1 });
		}
	} elsif ( $type == SDL_KEYUP ) {
		my $up = $event->key_name();
		warn "SDL_KEYUP ($type) = '$up'", $just_checking ? ' fake' : '', "\n";
		$self->key_up( $up );
		$key_active->{$up} = 0;
		$self->record_session('key_pressed', { $up => 0 });
		undef $pending_key;
	}

	warn "key_pressed = $pending_key\n" if ( $pending_key );

	return $pending_key;
}

=head2 key_active

Is key currently pressed on keyboard or in session?

  $self->key_active( 'left shift', 'right shift', 'a' );

=cut

sub key_active {
	my $self = shift;
	my @keys = @_;
	confess "Regexp is no longer supported" if ref($_[0]) eq 'Regexp';

	my $active = 0;
	foreach my $key ( @keys ) {
		$active++ if $key_active->{$key};
	}

	warn "## key_active(",dump(@keys),") = $active\n" if $active;
	return $active;
}

=head2 loop

Implement CPU run for C<$run_run> cycles inside SDL event loop

  $self->loop( sub {
  	my $run_for = shift;
	CPU::exec( $run_for );
	$self->render_vram;
  } );

=cut

sub loop {
	my $self = shift;
	my $exec = shift;

	confess "need coderef as argument" unless ref($exec) eq 'CODE';
	my $event = SDL::Event->new();

	while ( 1 ) {
		$self->key_pressed( 1 );
		$exec->($run_for);
	}
}

=head2 @flip

Exported helper array used to flip bytes (from character roms for example)

  my $flipped = $flip[ $byte ];

=cut

our @flip;

foreach my $i ( 0 .. 255 ) {
	my $t = 0;
	$i & 0b00000001 and $t = $t | 0b10000000;
	$i & 0b00000010 and $t = $t | 0b01000000;
	$i & 0b00000100 and $t = $t | 0b00100000;
	$i & 0b00001000 and $t = $t | 0b00010000;
	$i & 0b00010000 and $t = $t | 0b00001000;
	$i & 0b00100000 and $t = $t | 0b00000100;
	$i & 0b01000000 and $t = $t | 0b00000010;
	$i & 0b10000000 and $t = $t | 0b00000001;
	#warn "$i = $t\n";
	$flip[$i] = $t;
}

=head1 SEE ALSO

L<Orao> is sample implementation using this module

=head1 AUTHOR

Dobrica Pavlinusic, C<< <dpavlin@rot13.org> >>

=head1 COPYRIGHT & LICENSE

Copyright 2007 Dobrica Pavlinusic, All Rights Reserved.

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut

1;