Automatic horizontal integer scaling implementation on SMS/Genesis/potentially others
Posted: Wed Feb 10, 2021 3:43 pm
There's been a fair bit of talk lately about using custom aspect ratios with vscale_mode 1 to get integer scaling both horizontally and vertically. This works, but it's a bit of a nuisance. I've had a fiddle with the SMS core and implemented automatic horizontal integer scaling as an OSD option which can be set to round the scaling multiple to the nearest integer either above or below what the selected aspect ratio would produce with non-integer scaling. I'm not a programmer and what little I know of verilog, system verilog, and vhdl I learned trying to figure this out (and my previous modification of the SMS core), so it probably could have been done more cleanly, but my implementation seems to work, at least in my limited tests (via HDMI in video modes 0 and 8).
I changed the menu a little in SMS.sv, and the rest of the changes all took place in sys_top.v and ascal.vhd. Both of those files are in the sys section of the code, so changing them is maybe a no-no and this won't get accepted into the official core, but it also means that if my changes (maybe cleaned up a bit) are accepted into the framework then adding this feature to other cores should just require adding an item to the OSD menu.
I've made a branch to my fork of the SMS core here with this change implemented. The .rbf build is under releases as SMSHoriMenu.rbf. Anyone interested, please download it, try it out, see if I've broken anything, and let me know. Thanks.
I'll explain what I did and how. The target for this explanation is me a week ago, i.e. someone with very little idea how this stuff works. People who already know can probably skip over it. The short version is that I added code in the scaler which divides the horizontal output resolution by the horizontal input resolution. The result of that is rounded down to an integer, which I then multiply the input resolution by to get the new, integer scaled, output resolution.
In more detail, here's my code, starting with changes to SMS.sv:
This is the menu option and its transmission to sys_top.v. This seriously confused me for a while, but I'll talk about the mistakes I made trying to get this to work after I finish talking about how it works. Anyway, line 183 here adds an option to the Audio/Video menu of the SMS core for "Intgr Hori Scale" with options off, narrow, and wide. Changing this option changes status bits 30 and 31. Line 149 assigns the values of those bits to HORIZ_INT, and line 45 passes that information to the module (in sys_top.v) which calls this module.
In sys_top.v:
Line 320 here declares a two bit wire named horiz_int. Line 1471 is in the part of the file that calls the module (emu) I changed in SMS.sv, and takes the HORIZ_INT value from there and sets horiz_int to it. Line 320 is in the part of the file that calls the scaler (ascal.vhd), and passes that value through to "hint" in there. Maybe I should have been more consistent with my names, dunno. Or at least gone with h_int rather than hint.
In ascal.vhd:
Alright, this is the meat of it. Line 214 accepts the info from the OSD menu - does the user want horizontal integer scaling? Then 1716 through 1736 do the actual work. There are a whole lot of variables here, some of which are set by the inputs to the core, and some of which are derived from the inputs. Anything starting with o_h relates to output, and anything starting with i_h relates to input. The h is for horizontal, there's more code I didn't change that relates to vertical scaling. When sys_top.v calls the scaler module it requests an output resolution, but doesn't specify the input resolution. The scaler figures out the input resolution from the video source. hdisp, hmax and hmin are some of the output parameters requested by sys_top. hdisp is the total displayed horizontal resolution, including black areas at the edge of the screen. hmin is where the picture starts within that, and hmax is where the picture ends.
Line 1717 checks if hint has the value 00, which would indicate horizontal integer scaling is set to off. If so, it uses the normal output values as if I hadn't changed anything.
1722 checks if hint is 01, which would reflect the menu being set to Narrow integer scaling. 1723 sets the output horizontal size to integer scaling:
hmax-hmin + 1 is the output size for the specified aspect ratio with whatever vertical scaling is going on. These values come from sys_top, where they are derived from the aspect ratio setting. I didn't change any of that. +1 is because the numbers start from zero. For example, if I had full screen scaling on in video_mode 8, hmin would be 0, and hmax would be 1919, and the width 1920.
i_hmax + 1 is the size of the horizontal input. i_hmin will always be 0 (I think), so there's no need to subtract it to get the width, but again I need to add one.
Dividing the horizontal output by the horizontal input gives me the horizontal multiplier. Because these are integers, the result ignores the remainder, so 6.782 becomes 6, 4.2 becomes 4, and so on. That result is then multiplied by the input horizontal resolution to get the new horizontal output size. Simple!
Line 1724's ELSE covers the third menu option. If horizontal integer scaling is on and not set to narrow, the only other option is wide. The formula here is the same as for the narrow scaling, but I add one to the initial division result so that 6.782 becomes 7 and 4.2 becomes 5 and so on. This is slightly complicated by the possibility that the result will be wider than the screen, which I think resulted in no picture in one of my test builds, so I've put in a check so that if the result is too wide it'll revert to narrow scaling, which should never exceed the screen width (I think). It probably would have been more efficient to declare a variable, set the result of the wide calculation to it, and then check that variable against the display size and use it to set the output size rather than do the same calculation twice on subsequent lines, but as I mentioned I'm not a programmer.
Lines 1731 through 1733 are intended to protect against an output resolution which is narrower than the input screwing up the results. Dividing a smaller output by a larger input would give a result below 1 which would be rounded down to zero, resulting in an o_hsize of 0. I'm not sure if this part of the code is actually working, though. The output horizontal resolution seems to bottom out at the width of the OSD. Further evaluation required.
Lines 1734 and 1735 set where the image starts and stops on the screen. To get it centred, you want to halve the difference between the size of the image and the total display size, and start drawing there. Then add the total width to that value to get the stopping point. I had to subtract one from that value because the width is a number that starts from one, but the position starts from zero. I think that's why. In reality, I did it because turning integer scaling on without subtracting one here added an extra column of pixels to the right side of the image that shouldn't have been there.
And that's it! It's pretty simple, and probably could have been done more cleanly, but it's working. Aside from maybe the fringe case with an output narrower than the input. I don't think you can do an integer downscale, so I want it to revert to normal scaling in that situation, but I'm not sure my IF statement is working. Maybe I need to put the zero in quotes or something? I'll take another look later, but if anyone who actually knows what they're doing could tell me that'd be appreciated.
Some mistakes I made along the way:
Lots of leaving out semicolons in the code. Good thing the compiler picks up on this.
I screwed up the OSD menu option initially. The menu would freeze when I selected the A/V tab, but the game would continue running in the background. This really had me stumped for a while - I thought at first maybe I was setting bits of the status string that were already claimed by other options, so I changed to some different ones. Eventually I figured out that I'd used too many characters. Initially it was "Horizontal Integer Scaling,Off,Narrow,Wide". The menu is I think 27 characters wide, one of which is a colon to separate the title and the options. My title was using all of that by itself. Once I abbreviated it a bit the freezing stopped. And it turned out that the bits of the status string I'd changed to were assigned already so I had to change them back to where I'd had them originally.
At first nothing was happening when I changed the menu option. Initially I had my check for the output being narrower than the input in line 1717, so it was IF horizontal scaling is off OR the output is narrower than the input THEN don't do integer scaling. I forget what exactly I was using to determine if the output was narrower than the input but whatever it was was always returning a positive value so integer scaling was never used.
Line 1725, which checks if the output is going to be wider than the screen, wasn't initially there. Instead, if wide scaling was selected I would calculate o_hsize and then compare that to the display size and recalculate using narrow scaling if it was too wide. I don't really get why (maybe a delay in the value of o_hsize being set so the comparison would be to the wrong value), but this was producing a really strange picture - part of the right side of the image would be cut off and there would be what looked like vertical scanlines, and sometimes the whole image would shimmer. Adding the calculation to the IF statement before setting the value seems to have fixed that.
My first test build didn't include menu options, it just set the core to always do the narrow integer scaling. I had the non-integer scaling commented out. When I first added the menu option, I forgot to uncomment it. The result was a blank screen (but I could hear the game's title screen music). This was before I figured out that my too-long menu option was crashing the menu, so I'd get a blank screen and then bring up the OSD to try to change the settings and the OSD would crash. Very dispiriting.
Anyways, I hope there's some interest in this. The MiSTer template says "Basically it's prohibited to change any files in this folder (sys)", so I'm not sure if I've broken the rules here. I think a lot of people are interested in having horizontal integer scaling, and I think this could be added to other cores without too much difficulty once the kinks are ironed out of my code. If you're interested, please download my build and see how it works for you. Let me know what's wrong with it.
I changed the menu a little in SMS.sv, and the rest of the changes all took place in sys_top.v and ascal.vhd. Both of those files are in the sys section of the code, so changing them is maybe a no-no and this won't get accepted into the official core, but it also means that if my changes (maybe cleaned up a bit) are accepted into the framework then adding this feature to other cores should just require adding an item to the OSD menu.
I've made a branch to my fork of the SMS core here with this change implemented. The .rbf build is under releases as SMSHoriMenu.rbf. Anyone interested, please download it, try it out, see if I've broken anything, and let me know. Thanks.
I'll explain what I did and how. The target for this explanation is me a week ago, i.e. someone with very little idea how this stuff works. People who already know can probably skip over it. The short version is that I added code in the scaler which divides the horizontal output resolution by the horizontal input resolution. The result of that is rounded down to an integer, which I then multiply the input resolution by to get the new, integer scaled, output resolution.
In more detail, here's my code, starting with changes to SMS.sv:
Code: Select all
45 output [1:0] HORIZ_INT, // horizontal integer scaling setting
149 assign HORIZ_INT = status [31:30];
183 "P1OUV,Intgr Hori Scale,Off,Narrow,Wide;",
In sys_top.v:
Code: Select all
320 wire[1:0] horiz_int;
694 .hint (horiz_int),
1471 .HORIZ_INT(horiz_int),
In ascal.vhd:
Code: Select all
214 hint : IN std_logic_vector(1 DOWNTO 0);
1716 o_hdisp <=hdisp; -- <ASYNC> ?
1717 IF hint(1 DOWNTO 0)="00" THEN -- Horizontal integer scaling off
1718 o_hmin <=hmin; -- <ASYNC> ?
1719 o_hmax <=hmax; -- <ASYNC> ?
1720 o_hsize <=o_hmax - o_hmin + 1;
1721 ELSE --Horizontal integer scaling on
1722 IF hint(1 DOWNTO 0)="01" THEN --Narrow (round upscaling multiple down)
1723 o_hsize <= ((hmax-hmin + 1)/(i_hmax + 1))*(i_hmax + 1);
1724 ELSE --Wide (round upscaling multiple up)
1725 IF ((hmax-hmin + 1)/(i_hmax + 1) +1)*(i_hmax + 1) < o_hdisp THEN
1726 o_hsize <= ((hmax-hmin + 1)/(i_hmax + 1) +1)*(i_hmax + 1);
1727 ELSE --switch to narrow if image wider than total display
1728 o_hsize <= ((hmax-hmin + 1)/(i_hmax + 1))*(i_hmax + 1);
1729 END IF;
1730 END IF;
1731 IF o_hsize = 0 THEN -- No integer scaling if output smaller than input
1732 o_hsize <= hmax - hmin + 1;
1733 END IF;
1734 o_hmin <= (o_hdisp - o_hsize) / 2;
1735 o_hmax <= o_hmin + o_hsize -1;
1736 END IF;
Line 1717 checks if hint has the value 00, which would indicate horizontal integer scaling is set to off. If so, it uses the normal output values as if I hadn't changed anything.
1722 checks if hint is 01, which would reflect the menu being set to Narrow integer scaling. 1723 sets the output horizontal size to integer scaling:
Code: Select all
o_hsize <= ((hmax-hmin + 1)/(i_hmax + 1))*(i_hmax + 1);
i_hmax + 1 is the size of the horizontal input. i_hmin will always be 0 (I think), so there's no need to subtract it to get the width, but again I need to add one.
Dividing the horizontal output by the horizontal input gives me the horizontal multiplier. Because these are integers, the result ignores the remainder, so 6.782 becomes 6, 4.2 becomes 4, and so on. That result is then multiplied by the input horizontal resolution to get the new horizontal output size. Simple!
Line 1724's ELSE covers the third menu option. If horizontal integer scaling is on and not set to narrow, the only other option is wide. The formula here is the same as for the narrow scaling, but I add one to the initial division result so that 6.782 becomes 7 and 4.2 becomes 5 and so on. This is slightly complicated by the possibility that the result will be wider than the screen, which I think resulted in no picture in one of my test builds, so I've put in a check so that if the result is too wide it'll revert to narrow scaling, which should never exceed the screen width (I think). It probably would have been more efficient to declare a variable, set the result of the wide calculation to it, and then check that variable against the display size and use it to set the output size rather than do the same calculation twice on subsequent lines, but as I mentioned I'm not a programmer.
Lines 1731 through 1733 are intended to protect against an output resolution which is narrower than the input screwing up the results. Dividing a smaller output by a larger input would give a result below 1 which would be rounded down to zero, resulting in an o_hsize of 0. I'm not sure if this part of the code is actually working, though. The output horizontal resolution seems to bottom out at the width of the OSD. Further evaluation required.
Lines 1734 and 1735 set where the image starts and stops on the screen. To get it centred, you want to halve the difference between the size of the image and the total display size, and start drawing there. Then add the total width to that value to get the stopping point. I had to subtract one from that value because the width is a number that starts from one, but the position starts from zero. I think that's why. In reality, I did it because turning integer scaling on without subtracting one here added an extra column of pixels to the right side of the image that shouldn't have been there.
And that's it! It's pretty simple, and probably could have been done more cleanly, but it's working. Aside from maybe the fringe case with an output narrower than the input. I don't think you can do an integer downscale, so I want it to revert to normal scaling in that situation, but I'm not sure my IF statement is working. Maybe I need to put the zero in quotes or something? I'll take another look later, but if anyone who actually knows what they're doing could tell me that'd be appreciated.
Some mistakes I made along the way:
Lots of leaving out semicolons in the code. Good thing the compiler picks up on this.
I screwed up the OSD menu option initially. The menu would freeze when I selected the A/V tab, but the game would continue running in the background. This really had me stumped for a while - I thought at first maybe I was setting bits of the status string that were already claimed by other options, so I changed to some different ones. Eventually I figured out that I'd used too many characters. Initially it was "Horizontal Integer Scaling,Off,Narrow,Wide". The menu is I think 27 characters wide, one of which is a colon to separate the title and the options. My title was using all of that by itself. Once I abbreviated it a bit the freezing stopped. And it turned out that the bits of the status string I'd changed to were assigned already so I had to change them back to where I'd had them originally.
At first nothing was happening when I changed the menu option. Initially I had my check for the output being narrower than the input in line 1717, so it was IF horizontal scaling is off OR the output is narrower than the input THEN don't do integer scaling. I forget what exactly I was using to determine if the output was narrower than the input but whatever it was was always returning a positive value so integer scaling was never used.
Line 1725, which checks if the output is going to be wider than the screen, wasn't initially there. Instead, if wide scaling was selected I would calculate o_hsize and then compare that to the display size and recalculate using narrow scaling if it was too wide. I don't really get why (maybe a delay in the value of o_hsize being set so the comparison would be to the wrong value), but this was producing a really strange picture - part of the right side of the image would be cut off and there would be what looked like vertical scanlines, and sometimes the whole image would shimmer. Adding the calculation to the IF statement before setting the value seems to have fixed that.
My first test build didn't include menu options, it just set the core to always do the narrow integer scaling. I had the non-integer scaling commented out. When I first added the menu option, I forgot to uncomment it. The result was a blank screen (but I could hear the game's title screen music). This was before I figured out that my too-long menu option was crashing the menu, so I'd get a blank screen and then bring up the OSD to try to change the settings and the OSD would crash. Very dispiriting.
Anyways, I hope there's some interest in this. The MiSTer template says "Basically it's prohibited to change any files in this folder (sys)", so I'm not sure if I've broken the rules here. I think a lot of people are interested in having horizontal integer scaling, and I think this could be added to other cores without too much difficulty once the kinks are ironed out of my code. If you're interested, please download my build and see how it works for you. Let me know what's wrong with it.