/*-*- mode:unix-assembly; indent-tabs-mode:t; tab-width:8; coding:utf-8     -*-│
│ vi: set noet ft=asm ts=8 sw=8 fenc=utf-8                                 :vi │
╞══════════════════════════════════════════════════════════════════════════════╡
│ This is free and unencumbered software released into the public domain.      │
│                                                                              │
│ Anyone is free to copy, modify, publish, use, compile, sell, or              │
│ distribute this software, either in source code form or as a compiled        │
│ binary, for any purpose, commercial or non-commercial, and by any            │
│ means.                                                                       │
│                                                                              │
│ In jurisdictions that recognize copyright laws, the author or authors        │
│ of this software dedicate any and all copyright interest in the              │
│ software to the public domain. We make this dedication for the benefit       │
│ of the public at large and to the detriment of our heirs and                 │
│ successors. We intend this dedication to be an overt act of                  │
│ relinquishment in perpetuity of all present and future rights to this        │
│ software under copyright law.                                                │
│                                                                              │
│ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,              │
│ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF           │
│ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.       │
│ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR            │
│ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,        │
│ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR        │
│ OTHER DEALINGS IN THE SOFTWARE.                                              │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "ape/relocations.h"
#include "libc/macros.h"
#include "libc/runtime/mman.internal.h"
#include "libc/vga/vga.internal.h"

#define VGA_PREFER_GRAPH_HEIGHT \
	(VGA_PREFER_TTY_HEIGHT * VGA_ASSUME_CHAR_HEIGHT_PX)
#define VGA_PREFER_GRAPH_WIDTH  \
	(VGA_PREFER_TTY_WIDTH * VGA_ASSUME_CHAR_WIDTH_PX)
#define MAX_VESA_MODES_TO_TRACK 64

//	Mode information data structure, used internally.
	.set	"mi::type",0
	.set	"mi::bpp",1
	.set	"mi::width",2
	.set	"mi::height",4
	.set	"mi::mode_num",6
	.set	"mi::stride",8
	.set	"mi::fb",10
	.set	"mi::sizeof",14

//	Routine to activate additional VESA functionality if the user holds
//	down a magic key, when booting from bare metal.
//
//	@return	CF = 0 if we decided to set a new video mode, CF = 1 otherwise

	.real
	.code16
_rlinit_vesa:
	push	%es
	pushw	$0
	testb	$0b00000011,0x0417	# read keyboard shift state (as
	jnz	.doit			# given by BIOS's IRQ 1); if Shift
					# key pressed, activate code below
	push	%si			# display brief message on magic key
	mov	$REAL(str.inform),%si
	call	.puts
	pop	%si
	mov	$0x8300,%ax		# wait for the magic key for a short
	mov	$(1000000>>16),%cx	# period of time...
	mov	$(1000000&0xffff),%dx
	push	%ss
	pop	%es
	mov	%sp,%bx
	int	$0x15
	jc	.done
.wait:	pause
	testb	$0b00000011,0x0417
	jnz	.doit2
	cmpb	$0,%es:(%bx)
	jz	.wait
	call	.done_inform
	stc
.done:	pop	%ax
	pop	%es
	ret
.doit2:	mov	$0x8301,%ax		# we got the magic key; cancel the
	int	$0x15			# wait timer, & erase message
	call	.done_inform
.doit:	pop	%ax			# we got the magic key; do stuff
	pop	%es
	// fall through
	.endfn	_rlinit_vesa,globl,hidden

.do_vesa_rlinit:
	push	%eax
	push	%bx
	push	%edx
	push	%bp
	sub	$2+MAX_VESA_MODES_TO_TRACK*"mi::sizeof",%sp
	mov	%sp,%bp
	call	.gather_vesa_modes	# gather list of VESA modes
	jc	8f
	call	.get_default_mode	# choose a default mode to use
	jc	6f
	call	.choose_mode		# allow user to select different mode
	movw	%bx,%bp
	call	.snooze
	mov	"mi::mode_num"(%bp),%bx
	mov	$0x4f02,%ax
	int	$0x10
	cmp	$0x004f,%ax
	jnz	9f
	mov	"mi::type"(%bp),%al
	.set	mm,0x0500
	mov	%al,mm+"struct mman::pc_video_type"
	mov	"mi::width"(%bp),%ax
	mov	%ax,mm+"struct mman::pc_video_width"
	movzwl	"mi::height"(%bp),%edx
	mov	%dx,mm+"struct mman::pc_video_height"
	mov	"mi::fb"(%bp),%eax
	mov	%eax,mm+"struct mman::pc_video_framebuffer"
	movzwl	"mi::stride"(%bp),%eax
	mov	%ax,mm+"struct mman::pc_video_stride"
	imul	%edx,%eax
	mov	%eax,mm+"struct mman::pc_video_framebuffer_size"
	xor	%eax,%eax
	mov	%eax,mm+"struct mman::pc_video_framebuffer"+4
	mov	%eax,mm+"struct mman::pc_video_framebuffer_size"+4
	clc
5:	lahf
	add	$2+MAX_VESA_MODES_TO_TRACK*"mi::sizeof",%sp
	sahf
	pop	%bp
	pop	%edx
	pop	%bx
	pop	%eax
	ret
6:	mov	$REAL(str.no_mode),%si
7:	call	.puts
8:	call	.snooze
	stc
	jmp	5b
9:	mov	$REAL(str.bad_mode),%si
	jmp	7b
	.endfn	.do_vesa_rlinit

//	Clear the informational message on the magic key.
.done_inform:
	mov	$0x0a00|' ',%ax
	mov	$7,%bx
	mov	$str.inform.end-str.inform-1,%cx
	int	$0x10
	ret

//	Preliminarily choose a "default" VESA screen mode from a list of
//	gathered screen modes.
//
//	@return	%bx is such that %ss:(%bx) = internal struc. for default mode
//	@return	CF = 1 if no available modes, CF = 0 otherwise
.get_default_mode:
	push	%eax
	push	%cx
	push	%edx
	push	%edi
	push	%bp
	mov	(%bp),%cx
	jcxz	8f
	inc	%bp
	inc	%bp
	or	$-1,%esi		# %esi = best fit screen size + penalty
					# (to be recalculated later)
	mov	%bp,%bx			# %bx = pointer to best info. struct.
1:	mov	"mi::type"(%bp),%ax	# load mode type & bits per pixel
	ror	%ah			# convert bpp into penalty value:
	movzbl	%ah,%edi		# fewer bpp are better, but prefer
					# 16 bpp over 15 bpp
	cmp	$PC_VIDEO_TEXT,%al	# handle text modes specially
	jz	9f
	movzwl	"mi::width"(%bp),%eax
	jz	9f
	cmp	$VGA_PREFER_GRAPH_WIDTH,%ax # calculate screen size
	jb	3f
	movzwl	"mi::height"(%bp),%edx
	cmp	$VGA_PREFER_GRAPH_HEIGHT,%dx
	jb	3f
	imul	%edx,%eax
2:	add	%edi,%eax
	jc	3f
	cmp	%esi,%eax
	jnb	3f
	mov	%eax,%esi
	mov	%bp,%bx
3:	add	$"mi::sizeof",%bp
	loop	1b
	clc
7:	pop	%bp
	pop	%edi
	pop	%edx
	pop	%cx
	pop	%eax
	ret
8:	stc
	jmp	7b
9:	imul	$VGA_ASSUME_CHAR_WIDTH_PX,%eax
	cmp	$VGA_PREFER_GRAPH_WIDTH,%eax
	jb	2b
	movzwl	"mi::height"(%bp),%edx
	imul	$VGA_ASSUME_CHAR_HEIGHT_PX,%edx
	cmp	$VGA_PREFER_GRAPH_HEIGHT,%edx
	jb	3b
	imul	%edx,%eax
	jo	3b
	mov	$1<<31,%edi		# severely disparage text modes
	jmp	2b
	.endfn	.get_default_mode

//	Allow the user to choose a VESA screen mode to use.
//
//	@return	%bx is such that ss:(%bx) = internal struc. for chosen mode
.choose_mode:
	push	%ax
	push	%si
	push	%di
	mov	(%bp),%di
	imul	$"mi::sizeof",%di
	lea	2(%bp,%di),%di
1:	mov	$REAL(str.choose_mode),%si
	call	.puts
	mov	%ss:("mi::mode_num")(%bx),%ax
	call	.putx
	mov	%ss:("mi::width")(%bx),%ax
	call	.putd
	mov	%ss:("mi::height")(%bx),%ax
	call	.putd
	mov	%ss:("mi::bpp")(%bx),%al
	call	.putdb
	mov	$0,%ah
	int	$0x16
#define UPSCN 0x48
#define DNSCN 0x50
#define CRSCN 0x1c
	cmp	$DNSCN,%ah
	jz	3f
	cmp	$CRSCN,%ah
	jz	4f
	cmp	$UPSCN,%ah
	jnz	1b
	lea	2(%bp),%ax		# up arrow pressed
	cmp	%ax,%bx
	jz	1b
2:	sub	$"mi::sizeof",%bx
	jmp	1b
3:	add	$"mi::sizeof",%bx	# down arrow pressed
	cmp	%di,%bx
	jnz	1b
	jmp	2b
4:	call	.putnl			# Enter pressed
	pop	%di
	pop	%si
	pop	%ax
	ret
	.endfn	.choose_mode

.snooze:
	push	%ax
	push	%cx
	push	%dx
	mov	$0x86,%ah		# do a(nother) short few-second wait
	mov	$(2000000>>16),%cx
	mov	$(2000000&0xffff),%dx
	int	$0x15
	pop	%dx
	pop	%cx
	pop	%ax
	ret
	.endfn	.snooze

//	Dump a list of all the VESA VBE video modes that are usable on the
//	target machine.  Also gather a list of video modes and basic
//	information about these modes at %ss:(%bp).
.gather_vesa_modes:
	push	%ds
	push	%es
	push	%si
	push	%di
	push	%ss			# allocate 0x200 bytes on stack
	pop	%es			# for general VESA information
	sub	$0x200,%sp
	mov	$0x200/2,%cx
	mov	%sp,%di
	cld
	xor	%ax,%ax
	rep stosw
	mov	$0x4f00,%ax		# get general VESA information
	mov	%sp,%di
	movl	$/*"VBE2"*/0x32454256,%es:(%di)
	int	$0x10
	cmp	$0x004f,%ax		# if this fails, complain
	jnz	.fail2
	mov	$REAL(str.mode_list_start),%si
	call	.puts			# otherwise start iterating through
	lds	%es:0xe(%di),%si	# the returned video mode list
	movw	$0,(%bp)
	cld
.iter1:	lodsw
	inc	%ax
	jz	.done2
	dec	%ax
	call	.munge			# process mode number
	jc	.iter1
.iter2:	lodsw
	inc	%ax
	jz	.nl
	dec	%ax
	call	.munge			# process another mode number
	jc	.iter2
.iter3:	lodsw
	inc	%ax
	jz	.nl
	dec	%ax
	call	.munge			# process another mode number
	jc	.iter3
	call	.putnl
	jmp	.iter1			# ...and so on
.nl:	call	.putnl
	clc
.done2:	add	$0x200,%sp		# OK, we are finally done
	pop	%di
	pop	%si
	pop	%es
	pop	%ds
	ret
.fail2:	mov	$REAL(str.no_vesa),%si
	call	.puts
	stc
	jmp	.done2
	.endfn	.gather_vesa_modes

//	Display information on one video mode number, which should be in %ax.
//	If %ax is a mode which we can use, also update the mode information
//	buffer at %ss:(%bp).  Assume %es = %ss.
//
//	@return	CF = 0 if video mode is usable, CF = 1 otherwise
.munge:	push	%ax
	push	%bx
	push	%cx
	push	%si
	push	%di
	or	$1<<14,%ax		# force frame buffer mode
	xchg	%ax,%si			# remember mode number
	sub	$0x100,%sp		# allocate 0x100 stack bytes for
	mov	$0x100/2,%cx		# information on this mode; clear
	mov	%sp,%di			# the bytes
	cld
	xor	%ax,%ax
	rep stosw
	mov	%si,%cx			# get information on one mode
	mov	$0x4f01,%ax
	mov	%sp,%di
	int	$0x10
	cmp	$0x004f,%ax		# if error, skip mode
	jnz	.fail3
	mov	%es:(%di),%al
	mov	%al,%cl
	and	$0b00001011,%al		# also skip if mode is unusable, or
	cmp	$0b00001011,%al		# extra information unavailable, or
	jnz	.fail3			# is monochrome, or is graphics w/o
	mov	%cl,%al			# linear frame buffer
	and	$0b10010000,%al
	cmp	$0b00010000,%al
	jz	.fail3
	call	.video_type		# check if we know about the video
	jc	.fail3			# buffer type, & what exact type it is
	mov	(%bp),%bx		# if we are already tracking too many
	cmp	$MAX_VESA_MODES_TO_TRACK,%bx # VESA modes, also skip
	jnb	.fail3
	inc	%bx			# otherwise start noting down mode
	mov	%bx,(%bp)		# information...
	imul	$"mi::sizeof",%bx
	lea	2-"mi::sizeof"(%ebp,%ebx),%bx
	mov	%al,%ss:("mi::type")(%bx) # ...starting from frame buffer type
	call	.putsp			# echo and remember mode information
	call	.putsp
	test	$0b00010000,%cl		# first, echo mode attributes
	setnz	%al
	imul	$'G'-'T',%ax,%ax	# - 'G': graphics; 'T': text mode
	add	$'T',%al
	call	.putc
	call	.putsp
	xchg	%ax,%si			# then process
	mov	%ax,%ss:("mi::mode_num")(%bx) # - mode number
	call	.putx
	mov	%es:0x12(%di),%ax	# - mode width
	mov	%ax,%ss:("mi::width")(%bx)
	call	.putd
	mov	%es:0x14(%di),%ax	# - mode height
	mov	%ax,%ss:("mi::height")(%bx)
	call	.putd
	mov	%es:0x19(%di),%al	# - bits per pixel
	mov	%al,%ss:("mi::bpp")(%bx)
	call	.putdb
	mov	%es:0x10(%di),%ax	# - mode stride
	mov	%ax,%ss:("mi::stride")(%bx)
	testb	$0b00010000,%cl
	jz	.txt
	mov	%es:0x28(%di),%eax
.fb:	mov	%eax,%ss:("mi::fb")(%bx) # - frame buffer address
	clc
.done3:	lea	0x100(%esp),%sp
	pop	%di
	pop	%si
	pop	%cx
	pop	%bx
	pop	%ax
	ret
.fail3:	stc
	jmp	.done3
.txt:	movzwl	%es:8(%di),%eax		# for text mode, use window A as
	shl	$4,%eax			# frame buffer
	jmp	.fb
	.endfn	.munge

//	Check if the given video mode information uses a video buffer type
//	we know about, and say what type of video buffer it is.
//
//	@param	%es:(%di) = video mode information from int 0x10, %ax = 0x4f01
//	@param	%cl = low byte of video mode attributes (= %es:(%di))
//	@return	%al = video buffer type (for mman::pc_video_type)
//	@return	CF = 0 if video mode is usable, CF = 1 otherwise
.video_type:
	push	%bx
	push	%cx
	push	%edx
	mov	$PC_VIDEO_TEXT,%al	# if text mode, simply say so
	test	$0b00010000,%cl
	jz	.ok4
	cmpb	$6,%es:0x1b(%di)	# if graphics mode, check if direct
	jnz	.fail4			# color; bail out if not
	mov	%es:0x19(%di),%cl	# check actual color fields & bits
	mov	%es:0x1f(%di),%ax	# per pixel, against a list
	mov	%es:0x21(%di),%edx
	mov	$REAL(.type_list),%bx
.iter4:	cmp	%edx,%cs:4(%bx)
	jnz	.next
	cmp	%cl,%cs:1(%bx)
	jnz	.next
	cmp	%ax,%cs:2(%bx)
	jz	.found
.next:	add	$8,%bx
	cmp	$REAL(.type_list_end),%bx
	jnz	.iter4
.fail4:	stc				# unsupported mode; return failure
	jmp	.done4
.found:	mov	%cs:(%bx),%al		# this mode is supported; return the
.ok4:	clc				# corresponding type code
.done4:	pop	%edx
	pop	%cx
	pop	%bx
	ret

//	Output a string via BIOS.
.puts:	cld
	push	%si
0:	lodsb
	test	%al,%al
	jz	1f
	call	.putc
	jmp	0b
1:	pop	%si
	ret
	.endfn	.puts

//	Output a 16-bit number in decimal via BIOS.
.putd:	push	%ax
	push	%cx
	push	%dx
	push	%si
	push	%ds
	push	%ss
	pop	%ds
	sub	$8,%sp
	lea	4(%esp),%si
	mov	$10,%cx
	xor	%dx,%dx
	div	%cx
	add	$'0'|' '<<8,%dx
	mov	%dx,(%si)
	movb	$0,2(%si)
1:	mov	$' ',%dl
	test	%ax,%ax
	jz	2f
	xor	%dx,%dx
	div	%cx
	add	$'0',%dl
2:	dec	%si
	mov	%dl,(%si)
	cmp	%sp,%si
	jnz	1b
	call	.puts
	add	$8,%sp
	pop	%ds
	pop	%si
	pop	%dx
	pop	%cx
	pop	%ax
	ret
	.endfn	.putd

//	Output an 8-bit number in decimal via BIOS.
.putdb:	push	%ax
	mov	%al,%ah
	cmp	$100,%al
	jnb	3f
	mov	$' ',%al
1:	call	.putc
	mov	%ah,%al
	aam
	testb	%ah,%ah
	jz	5f
	add	$'0'|'0'<<8,%ax
2:	xchg	%al,%ah
	call	.putc
	mov	%ah,%al
	call	.putc
	pop	%ax
	ret
3:	cmp	$200,%al
	jnb	4f
	sub	$100,%ah
	mov	$'1',%al
	jmp	1b
4:	sub	$200,%ah
	mov	$'2',%al
	jmp	1b
5:	add	$'0'|' '<<8,%ax
	jmp	2b

//	Output a number in hexadecimal via BIOS.
.putx:	push	%ax
	push	%bx
	push	%cx
	xchg	%ax,%bx
	mov	$'0',%al
	call	.putc
	mov	$'x',%al
	call	.putc
	mov	$4,%cx
0:	rol	$4,%bx
	mov	%bl,%al
	and	$0b00001111,%al
	add	$'0',%al
	cmp	$'9',%al
	jna	1f
	add	$'a'-'9'-1,%al
1:	call	.putc
	loop	0b
	pop	%cx
	pop	%bx
	pop	%ax
	.endfn	.putx
	// fall through

//	Output a character via BIOS.
.putsp:	mov	$' ',%al
	.endfn	.putsp
	// fall through

.putc:	push	%ax
	push	%bx
	mov	$7,%bx
	mov	$0x0e,%ah
	int	$0x10
	pop	%bx
	pop	%ax
	ret
	.endfn	.putc

.putnl:
	mov	$'\r',%al
	call	.putc
	mov	$'\n',%al
	jmp	.putc
	.endfn	.putnl

str.inform:
#define SHGLYPH	"\x7f"
	.ascii	"\rinfo: press ",SHGLYPH,"Shift "
	.asciz	"to switch video mode\r"
str.inform.end:
	.endobj	str.inform
str.no_vesa:
	.asciz	"info: no VESA\r\n"
	.endobj	str.no_vesa
str.choose_mode:
#define UPGLYPH "\x18"
#define DNGLYPH "\x19"
#define CRGLYPH "\x1b\xd9"
	.ascii	"\rchoose video mode (",UPGLYPH," ",DNGLYPH," ",CRGLYPH
	.asciz	"): "
	.endobj	str.choose_mode
str.no_mode:
	.asciz	"info: no usable video mode\r\n"
	.endobj	str.no_mode
str.bad_mode:
	.asciz	"info: mode switch fail\r\n"
	.endobj	str.bad_mode
str.mode_list_start:
	.ascii	"info: VESA video modes:\r\n"
	.ascii	"     mode#     X     Y bpp"
	.ascii	"     mode#     X     Y bpp"
	.asciz	"     mode#     X     Y bpp\r\n"
	.endobj	str.mode_list_start

.type_list:
//		              ┌value to use for mman::pc_video_type
//		              │    ┌bits per pixel
//		              │    │  ┌red field size (in bits)
//		              │    │  │  ┌red field position (bits)
//		              │    │  │  │  ┌green field size
//		              │    │  │  │  │  ┌green field position
//		              │    │  │  │  │  │  ┌blue field size
//		              │    │  │  │  │  │  │  ┌blue field position
//		              │    │  │  │  │  │  │  │
	.byte	PC_VIDEO_BGR565,  16, 5,11, 6, 5, 5, 0
	.byte	PC_VIDEO_BGR555,  15, 5,10, 5, 5, 5, 0
	.byte	PC_VIDEO_BGR555,  16, 5,10, 5, 5, 5, 0
	.byte	PC_VIDEO_RGBX8888,32, 8, 0, 8, 8, 8,16
	.byte	PC_VIDEO_BGRX8888,32, 8,16, 8, 8, 8, 0
	.endobj	.type_list
.type_list_end:

	.previous
